[NFCT.TETHER.15] Attach BPF program in the mainline module

Migrate Maze's BPF program attaching and detaching functions from
system/netd/server/OffloadUtils.{c, h} to tethering module.

Test: atest TetheringCoverageTests
Test case #1:
Enable WiFi hotspot and check tc filters are added or removed on both
wlan1 and rmnet_data#.

$ adb shell tc filter show dev wlan1 ingress
filter protocol ipv6 pref 1 bpf chain 0
filter protocol ipv6 pref 1 bpf chain 0 handle 0x1
prog_offload_schedcls_tether_upstream6_ether:[*fsobj] direct-action
not_in_hw id 2 tag 7cf020cc09a7c982
filter protocol ip pref 2 bpf chain 0
filter protocol ip pref 2 bpf chain 0 handle 0x1
prog_offload_schedcls_tether_upstream4_ether:[*fsobj] direct-action
not_in_hw id 7 tag 2f87d55b636c082c

$ adb shell tc filter show dev rmnet_data2 ingress;
filter protocol ipv6 pref 1 bpf chain 0
filter protocol ipv6 pref 1 bpf chain 0 handle 0x1
prog_offload_schedcls_tether_downstream6_rawip:[*fsobj] direct-action
not_in_hw id 3 tag 8b3885b75bd261de
filter protocol ip pref 2 bpf chain 0
filter protocol ip pref 2 bpf chain 0 handle 0x1
prog_offload_schedcls_tether_downstream4_rawip:[*fsobj] direct-action
not_in_hw id 6 tag b1c9478c91f8df9a

Test case #2:
Enable USB tethering and check tc filters are added or removed on both
rndis0 and rmnet_data#.

Test case #3:
Enable WiFi and USB tethering and check tc filter are added or removed
on rndis0, wlan1 and rmnet_data#.

Change-Id: I3f9a65043271bc8f5bf1b82ae505c471625ca9de
diff --git a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
index 4e615a1..f27c831 100644
--- a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
@@ -159,6 +159,18 @@
     }
 
     @Override
+    public boolean attachProgram(String iface, boolean downstream) {
+        /* no op */
+        return true;
+    }
+
+    @Override
+    public boolean detachProgram(String iface) {
+        /* no op */
+        return true;
+    }
+
+    @Override
     public String toString() {
         return "Netd used";
     }
diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
index 03d2443..4f7fe65 100644
--- a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
@@ -31,6 +31,7 @@
 import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
 import com.android.networkstack.tethering.BpfMap;
+import com.android.networkstack.tethering.BpfUtils;
 import com.android.networkstack.tethering.Tether4Key;
 import com.android.networkstack.tethering.Tether4Value;
 import com.android.networkstack.tethering.Tether6Value;
@@ -42,6 +43,7 @@
 import com.android.networkstack.tethering.TetherUpstream6Key;
 
 import java.io.FileDescriptor;
+import java.io.IOException;
 
 /**
  * Bpf coordinator class for API shims.
@@ -358,6 +360,32 @@
         return true;
     }
 
+    @Override
+    public boolean attachProgram(String iface, boolean downstream) {
+        if (!isInitialized()) return false;
+
+        try {
+            BpfUtils.attachProgram(iface, downstream);
+        } catch (IOException e) {
+            mLog.e("Could not attach program: " + e);
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public boolean detachProgram(String iface) {
+        if (!isInitialized()) return false;
+
+        try {
+            BpfUtils.detachProgram(iface);
+        } catch (IOException e) {
+            mLog.e("Could not detach program: " + e);
+            return false;
+        }
+        return true;
+    }
+
     private String mapStatus(BpfMap m, String name) {
         return name + "{" + (m != null ? "OK" : "ERROR") + "}";
     }
diff --git a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
index c61c449..b7b4c47 100644
--- a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
+++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
@@ -143,5 +143,19 @@
      * Deletes a tethering IPv4 offload rule from the appropriate BPF map.
      */
     public abstract boolean tetherOffloadRuleRemove(boolean downstream, @NonNull Tether4Key key);
+
+    /**
+     * Attach BPF program.
+     *
+     * TODO: consider using InterfaceParams to replace interface name.
+     */
+    public abstract boolean attachProgram(@NonNull String iface, boolean downstream);
+
+    /**
+     * Detach BPF program.
+     *
+     * TODO: consider using InterfaceParams to replace interface name.
+     */
+    public abstract boolean detachProgram(@NonNull String iface);
 }
 
diff --git a/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp b/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp
new file mode 100644
index 0000000..308dfb9
--- /dev/null
+++ b/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <arpa/inet.h>
+#include <jni.h>
+#include <linux/if_arp.h>
+#include <linux/if_ether.h>
+#include <linux/netlink.h>
+#include <linux/pkt_cls.h>
+#include <linux/pkt_sched.h>
+#include <linux/rtnetlink.h>
+#include <nativehelper/JNIHelp.h>
+#include <net/if.h>
+#include <stdio.h>
+#include <sys/socket.h>
+
+// TODO: use unique_fd.
+#define BPF_FD_JUST_USE_INT
+#include "BpfSyscallWrappers.h"
+#include "bpf_tethering.h"
+#include "nativehelper/scoped_utf_chars.h"
+
+// The maximum length of TCA_BPF_NAME. Sync from net/sched/cls_bpf.c.
+#define CLS_BPF_NAME_LEN 256
+
+namespace android {
+// Sync from system/netd/server/NetlinkCommands.h
+const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK;
+const sockaddr_nl KERNEL_NLADDR = {AF_NETLINK, 0, 0, 0};
+
+// TODO: move to frameworks/libs/net/common/native for sharing with
+// system/netd/server/OffloadUtils.{c, h}.
+static void sendAndProcessNetlinkResponse(JNIEnv* env, const void* req, int len) {
+    int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);  // TODO: use unique_fd
+    if (fd == -1) {
+        jniThrowExceptionFmt(env, "java/io/IOException",
+                             "socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE): %s",
+                             strerror(errno));
+        return;
+    }
+
+    static constexpr int on = 1;
+    if (setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &on, sizeof(on))) {
+        jniThrowExceptionFmt(env, "java/io/IOException",
+                             "setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, %d)", on);
+        close(fd);
+        return;
+    }
+
+    // this is needed to get valid strace netlink parsing, it allocates the pid
+    if (bind(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR))) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "bind(fd, {AF_NETLINK, 0, 0}): %s",
+                             strerror(errno));
+        close(fd);
+        return;
+    }
+
+    // we do not want to receive messages from anyone besides the kernel
+    if (connect(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR))) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "connect(fd, {AF_NETLINK, 0, 0}): %s",
+                             strerror(errno));
+        close(fd);
+        return;
+    }
+
+    int rv = send(fd, req, len, 0);
+
+    if (rv == -1) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "send(fd, req, len, 0): %s",
+                             strerror(errno));
+        close(fd);
+        return;
+    }
+
+    if (rv != len) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "send(fd, req, len, 0): %s",
+                             strerror(EMSGSIZE));
+        close(fd);
+        return;
+    }
+
+    struct {
+        nlmsghdr h;
+        nlmsgerr e;
+        char buf[256];
+    } resp = {};
+
+    rv = recv(fd, &resp, sizeof(resp), MSG_TRUNC);
+
+    if (rv == -1) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "recv() failed: %s", strerror(errno));
+        close(fd);
+        return;
+    }
+
+    if (rv < (int)NLMSG_SPACE(sizeof(struct nlmsgerr))) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "recv() returned short packet: %d", rv);
+        close(fd);
+        return;
+    }
+
+    if (resp.h.nlmsg_len != (unsigned)rv) {
+        jniThrowExceptionFmt(env, "java/io/IOException",
+                             "recv() returned invalid header length: %d != %d", resp.h.nlmsg_len,
+                             rv);
+        close(fd);
+        return;
+    }
+
+    if (resp.h.nlmsg_type != NLMSG_ERROR) {
+        jniThrowExceptionFmt(env, "java/io/IOException",
+                             "recv() did not return NLMSG_ERROR message: %d", resp.h.nlmsg_type);
+        close(fd);
+        return;
+    }
+
+    if (resp.e.error) {  // returns 0 on success
+        jniThrowExceptionFmt(env, "java/io/IOException", "NLMSG_ERROR message return error: %s",
+                             strerror(-resp.e.error));
+    }
+    close(fd);
+    return;
+}
+
+static int hardwareAddressType(const char* interface) {
+    int fd = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+    if (fd < 0) return -errno;
+
+    struct ifreq ifr = {};
+    // We use strncpy() instead of strlcpy() since kernel has to be able
+    // to handle non-zero terminated junk passed in by userspace anyway,
+    // and this way too long interface names (more than IFNAMSIZ-1 = 15
+    // characters plus terminating NULL) will not get truncated to 15
+    // characters and zero-terminated and thus potentially erroneously
+    // match a truncated interface if one were to exist.
+    strncpy(ifr.ifr_name, interface, sizeof(ifr.ifr_name));
+
+    int rv;
+    if (ioctl(fd, SIOCGIFHWADDR, &ifr, sizeof(ifr))) {
+        rv = -errno;
+    } else {
+        rv = ifr.ifr_hwaddr.sa_family;
+    }
+
+    close(fd);
+    return rv;
+}
+
+static jboolean com_android_networkstack_tethering_BpfUtils_isEthernet(JNIEnv* env, jobject clazz,
+                                                                       jstring iface) {
+    ScopedUtfChars interface(env, iface);
+
+    int rv = hardwareAddressType(interface.c_str());
+    if (rv < 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException",
+                             "Get hardware address type of interface %s failed: %s",
+                             interface.c_str(), strerror(-rv));
+        return false;
+    }
+
+    switch (rv) {
+        case ARPHRD_ETHER:
+            return true;
+        case ARPHRD_NONE:
+        case ARPHRD_RAWIP:  // in Linux 4.14+ rmnet support was upstreamed and this is 519
+        case 530:           // this is ARPHRD_RAWIP on some Android 4.9 kernels with rmnet
+            return false;
+        default:
+            jniThrowExceptionFmt(env, "java/io/IOException",
+                                 "Unknown hardware address type %s on interface %s", rv,
+                                 interface.c_str());
+            return false;
+    }
+}
+
+// tc filter add dev .. in/egress prio 1 protocol ipv6/ip bpf object-pinned /sys/fs/bpf/...
+// direct-action
+static void com_android_networkstack_tethering_BpfUtils_tcFilterAddDevBpf(
+        JNIEnv* env, jobject clazz, jint ifIndex, jboolean ingress, jshort prio, jshort proto,
+        jstring bpfProgPath) {
+    ScopedUtfChars pathname(env, bpfProgPath);
+
+    const int bpfFd = bpf::retrieveProgram(pathname.c_str());
+    if (bpfFd == -1) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "retrieveProgram failed %s",
+                             strerror(errno));
+        return;
+    }
+
+    struct {
+        nlmsghdr n;
+        tcmsg t;
+        struct {
+            nlattr attr;
+            // The maximum classifier name length is defined as IFNAMSIZ.
+            // See tcf_proto_ops in include/net/sch_generic.h.
+            char str[NLMSG_ALIGN(IFNAMSIZ)];
+        } kind;
+        struct {
+            nlattr attr;
+            struct {
+                nlattr attr;
+                __u32 u32;
+            } fd;
+            struct {
+                nlattr attr;
+                char str[NLMSG_ALIGN(CLS_BPF_NAME_LEN)];
+            } name;
+            struct {
+                nlattr attr;
+                __u32 u32;
+            } flags;
+        } options;
+    } req = {
+            .n =
+                    {
+                            .nlmsg_len = sizeof(req),
+                            .nlmsg_type = RTM_NEWTFILTER,
+                            .nlmsg_flags = NETLINK_REQUEST_FLAGS | NLM_F_EXCL | NLM_F_CREATE,
+                    },
+            .t =
+                    {
+                            .tcm_family = AF_UNSPEC,
+                            .tcm_ifindex = ifIndex,
+                            .tcm_handle = TC_H_UNSPEC,
+                            .tcm_parent = TC_H_MAKE(TC_H_CLSACT,
+                                                    ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS),
+                            .tcm_info = static_cast<__u32>((static_cast<uint16_t>(prio) << 16) |
+                                                           htons(static_cast<uint16_t>(proto))),
+                    },
+            .kind =
+                    {
+                            .attr =
+                                    {
+                                            .nla_len = sizeof(req.kind),
+                                            .nla_type = TCA_KIND,
+                                    },
+                            // Classifier name. See cls_bpf_ops in net/sched/cls_bpf.c.
+                            .str = "bpf",
+                    },
+            .options =
+                    {
+                            .attr =
+                                    {
+                                            .nla_len = sizeof(req.options),
+                                            .nla_type = NLA_F_NESTED | TCA_OPTIONS,
+                                    },
+                            .fd =
+                                    {
+                                            .attr =
+                                                    {
+                                                            .nla_len = sizeof(req.options.fd),
+                                                            .nla_type = TCA_BPF_FD,
+                                                    },
+                                            .u32 = static_cast<__u32>(bpfFd),
+                                    },
+                            .name =
+                                    {
+                                            .attr =
+                                                    {
+                                                            .nla_len = sizeof(req.options.name),
+                                                            .nla_type = TCA_BPF_NAME,
+                                                    },
+                                            // Visible via 'tc filter show', but
+                                            // is overwritten by strncpy below
+                                            .str = "placeholder",
+                                    },
+                            .flags =
+                                    {
+                                            .attr =
+                                                    {
+                                                            .nla_len = sizeof(req.options.flags),
+                                                            .nla_type = TCA_BPF_FLAGS,
+                                                    },
+                                            .u32 = TCA_BPF_FLAG_ACT_DIRECT,
+                                    },
+                    },
+    };
+
+    snprintf(req.options.name.str, sizeof(req.options.name.str), "%s:[*fsobj]",
+            basename(pathname.c_str()));
+
+    // The exception may be thrown from sendAndProcessNetlinkResponse. Close the file descriptor of
+    // BPF program before returning the function in any case.
+    sendAndProcessNetlinkResponse(env, &req, sizeof(req));
+    close(bpfFd);
+}
+
+// tc filter del dev .. in/egress prio .. protocol ..
+static void com_android_networkstack_tethering_BpfUtils_tcFilterDelDev(JNIEnv* env, jobject clazz,
+                                                                       jint ifIndex,
+                                                                       jboolean ingress,
+                                                                       jshort prio, jshort proto) {
+    const struct {
+        nlmsghdr n;
+        tcmsg t;
+    } req = {
+            .n =
+                    {
+                            .nlmsg_len = sizeof(req),
+                            .nlmsg_type = RTM_DELTFILTER,
+                            .nlmsg_flags = NETLINK_REQUEST_FLAGS,
+                    },
+            .t =
+                    {
+                            .tcm_family = AF_UNSPEC,
+                            .tcm_ifindex = ifIndex,
+                            .tcm_handle = TC_H_UNSPEC,
+                            .tcm_parent = TC_H_MAKE(TC_H_CLSACT,
+                                                    ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS),
+                            .tcm_info = static_cast<__u32>((static_cast<uint16_t>(prio) << 16) |
+                                                           htons(static_cast<uint16_t>(proto))),
+                    },
+    };
+
+    sendAndProcessNetlinkResponse(env, &req, sizeof(req));
+}
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+        /* name, signature, funcPtr */
+        {"isEthernet", "(Ljava/lang/String;)Z",
+         (void*)com_android_networkstack_tethering_BpfUtils_isEthernet},
+        {"tcFilterAddDevBpf", "(IZSSLjava/lang/String;)V",
+         (void*)com_android_networkstack_tethering_BpfUtils_tcFilterAddDevBpf},
+        {"tcFilterDelDev", "(IZSS)V",
+         (void*)com_android_networkstack_tethering_BpfUtils_tcFilterDelDev},
+};
+
+int register_com_android_networkstack_tethering_BpfUtils(JNIEnv* env) {
+    return jniRegisterNativeMethods(env, "com/android/networkstack/tethering/BpfUtils", gMethods,
+                                    NELEM(gMethods));
+}
+
+};  // namespace android
diff --git a/Tethering/jni/onload.cpp b/Tethering/jni/onload.cpp
index e31da60..02e602d 100644
--- a/Tethering/jni/onload.cpp
+++ b/Tethering/jni/onload.cpp
@@ -25,6 +25,7 @@
 int register_android_net_util_TetheringUtils(JNIEnv* env);
 int register_com_android_networkstack_tethering_BpfMap(JNIEnv* env);
 int register_com_android_networkstack_tethering_BpfCoordinator(JNIEnv* env);
+int register_com_android_networkstack_tethering_BpfUtils(JNIEnv* env);
 
 extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
     JNIEnv *env;
@@ -39,6 +40,8 @@
 
     if (register_com_android_networkstack_tethering_BpfCoordinator(env) < 0) return JNI_ERR;
 
+    if (register_com_android_networkstack_tethering_BpfUtils(env) < 0) return JNI_ERR;
+
     return JNI_VERSION_1_6;
 }
 
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 194737a..e5380e0 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -1291,6 +1291,7 @@
             // Sometimes interfaces are gone before we get
             // to remove their rules, which generates errors.
             // Just do the best we can.
+            mBpfCoordinator.maybeDetachProgram(mIfaceName, upstreamIface);
             try {
                 mNetd.ipfwdRemoveInterfaceForward(mIfaceName, upstreamIface);
             } catch (RemoteException | ServiceSpecificException e) {
@@ -1334,6 +1335,7 @@
                     mUpstreamIfaceSet = newUpstreamIfaceSet;
 
                     for (String ifname : added) {
+                        mBpfCoordinator.maybeAttachProgram(mIfaceName, ifname);
                         try {
                             mNetd.tetherAddForward(mIfaceName, ifname);
                             mNetd.ipfwdAddInterfaceForward(mIfaceName, ifname);
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 7c52716..8df3045 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -28,6 +28,8 @@
 import static android.system.OsConstants.ETH_P_IP;
 import static android.system.OsConstants.ETH_P_IPV6;
 
+import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM;
+import static com.android.networkstack.tethering.BpfUtils.UPSTREAM;
 import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
 
 import android.app.usage.NetworkStatsManager;
@@ -92,9 +94,6 @@
         System.loadLibrary("tetherutilsjni");
     }
 
-    static final boolean DOWNSTREAM = true;
-    static final boolean UPSTREAM = false;
-
     private static final String TAG = BpfCoordinator.class.getSimpleName();
     private static final int DUMP_TIMEOUT_MS = 10_000;
     private static final MacAddress NULL_MAC_ADDRESS = MacAddress.fromString(
@@ -118,7 +117,6 @@
         return makeMapPath((downstream ? "downstream" : "upstream") + ipVersion);
     }
 
-
     @VisibleForTesting
     enum StatsType {
         STATS_PER_IFACE,
@@ -218,6 +216,9 @@
     // is okay for now because there have only one upstream generally.
     private final HashMap<Inet4Address, Integer> mIpv4UpstreamIndices = new HashMap<>();
 
+    // Map for upstream and downstream pair.
+    private final HashMap<String, HashSet<String>> mForwardingPairs = new HashMap<>();
+
     // Runnable that used by scheduling next polling of stats.
     private final Runnable mScheduledPollingTask = () -> {
         updateForwardedStats();
@@ -691,6 +692,37 @@
         }
     }
 
+    /**
+     * Attach BPF program
+     *
+     * TODO: consider error handling if the attach program failed.
+     */
+    public void maybeAttachProgram(@NonNull String intIface, @NonNull String extIface) {
+        if (forwardingPairExists(intIface, extIface)) return;
+
+        boolean firstDownstreamForThisUpstream = !isAnyForwardingPairOnUpstream(extIface);
+        forwardingPairAdd(intIface, extIface);
+
+        mBpfCoordinatorShim.attachProgram(intIface, UPSTREAM);
+        // Attach if the upstream is the first time to be used in a forwarding pair.
+        if (firstDownstreamForThisUpstream) {
+            mBpfCoordinatorShim.attachProgram(extIface, DOWNSTREAM);
+        }
+    }
+
+    /**
+     * Detach BPF program
+     */
+    public void maybeDetachProgram(@NonNull String intIface, @NonNull String extIface) {
+        forwardingPairRemove(intIface, extIface);
+
+        // Detaching program may fail because the interface has been removed already.
+        mBpfCoordinatorShim.detachProgram(intIface);
+        // Detach if no more forwarding pair is using the upstream.
+        if (!isAnyForwardingPairOnUpstream(extIface)) {
+            mBpfCoordinatorShim.detachProgram(extIface);
+        }
+    }
 
     // TODO: make mInterfaceNames accessible to the shim and move this code to there.
     private String getIfName(long ifindex) {
@@ -1227,6 +1259,33 @@
         return false;
     }
 
+    private void forwardingPairAdd(@NonNull String intIface, @NonNull String extIface) {
+        if (!mForwardingPairs.containsKey(extIface)) {
+            mForwardingPairs.put(extIface, new HashSet<String>());
+        }
+        mForwardingPairs.get(extIface).add(intIface);
+    }
+
+    private void forwardingPairRemove(@NonNull String intIface, @NonNull String extIface) {
+        HashSet<String> downstreams = mForwardingPairs.get(extIface);
+        if (downstreams == null) return;
+        if (!downstreams.remove(intIface)) return;
+
+        if (downstreams.isEmpty()) {
+            mForwardingPairs.remove(extIface);
+        }
+    }
+
+    private boolean forwardingPairExists(@NonNull String intIface, @NonNull String extIface) {
+        if (!mForwardingPairs.containsKey(extIface)) return false;
+
+        return mForwardingPairs.get(extIface).contains(intIface);
+    }
+
+    private boolean isAnyForwardingPairOnUpstream(@NonNull String extIface) {
+        return mForwardingPairs.containsKey(extIface);
+    }
+
     @NonNull
     private NetworkStats buildNetworkStats(@NonNull StatsType type, int ifIndex,
             @NonNull final ForwardedStats diff) {
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfUtils.java b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java
new file mode 100644
index 0000000..289452c
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.networkstack.tethering;
+
+import static android.system.OsConstants.ETH_P_IP;
+import static android.system.OsConstants.ETH_P_IPV6;
+
+import android.net.util.InterfaceParams;
+
+import androidx.annotation.NonNull;
+
+import java.io.IOException;
+
+/**
+ * The classes and the methods for BPF utilization.
+ *
+ * {@hide}
+ */
+public class BpfUtils {
+    static {
+        System.loadLibrary("tetherutilsjni");
+    }
+
+    // For better code clarity when used for 'bool ingress' parameter.
+    static final boolean EGRESS = false;
+    static final boolean INGRESS = true;
+
+    // For better code clarify when used for 'bool downstream' parameter.
+    //
+    // This is talking about the direction of travel of the offloaded packets.
+    //
+    // Upstream means packets heading towards the internet/uplink (upload),
+    // thus for tethering this is attached to ingress on the downstream interface,
+    // while for clat this is attached to egress on the v4-* clat interface.
+    //
+    // Downstream means packets coming from the internet/uplink (download), thus
+    // for both clat and tethering this is attached to ingress on the upstream interface.
+    static final boolean DOWNSTREAM = true;
+    static final boolean UPSTREAM = false;
+
+    // The priority of clat/tether hooks - smaller is higher priority.
+    // TC tether is higher priority then TC clat to match XDP winning over TC.
+    // Sync from system/netd/server/OffloadUtils.h.
+    static final short PRIO_TETHER6 = 1;
+    static final short PRIO_TETHER4 = 2;
+    static final short PRIO_CLAT = 3;
+
+    private static String makeProgPath(boolean downstream, int ipVersion, boolean ether) {
+        String path = "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_"
+                + (downstream ? "downstream" : "upstream")
+                + ipVersion + "_"
+                + (ether ? "ether" : "rawip");
+        return path;
+    }
+
+    /**
+     * Attach BPF program
+     *
+     * TODO: use interface index to replace interface name.
+     */
+    public static void attachProgram(@NonNull String iface, boolean downstream)
+            throws IOException {
+        final InterfaceParams params = InterfaceParams.getByName(iface);
+        if (params == null) {
+            throw new IOException("Fail to get interface params for interface " + iface);
+        }
+
+        boolean ether;
+        try {
+            ether = isEthernet(iface);
+        } catch (IOException e) {
+            throw new IOException("isEthernet(" + params.index + "[" + iface + "]) failure: " + e);
+        }
+
+        try {
+            // tc filter add dev .. ingress prio 1 protocol ipv6 bpf object-pinned /sys/fs/bpf/...
+            // direct-action
+            tcFilterAddDevBpf(params.index, INGRESS, PRIO_TETHER6, (short) ETH_P_IPV6,
+                    makeProgPath(downstream, 6, ether));
+        } catch (IOException e) {
+            throw new IOException("tc filter add dev (" + params.index + "[" + iface
+                    + "]) ingress prio PRIO_TETHER6 protocol ipv6 failure: " + e);
+        }
+
+        try {
+            // tc filter add dev .. ingress prio 2 protocol ip bpf object-pinned /sys/fs/bpf/...
+            // direct-action
+            tcFilterAddDevBpf(params.index, INGRESS, PRIO_TETHER4, (short) ETH_P_IP,
+                    makeProgPath(downstream, 4, ether));
+        } catch (IOException e) {
+            throw new IOException("tc filter add dev (" + params.index + "[" + iface
+                    + "]) ingress prio PRIO_TETHER4 protocol ip failure: " + e);
+        }
+    }
+
+    /**
+     * Detach BPF program
+     *
+     * TODO: use interface index to replace interface name.
+     */
+    public static void detachProgram(@NonNull String iface) throws IOException {
+        final InterfaceParams params = InterfaceParams.getByName(iface);
+        if (params == null) {
+            throw new IOException("Fail to get interface params for interface " + iface);
+        }
+
+        try {
+            // tc filter del dev .. ingress prio 1 protocol ipv6
+            tcFilterDelDev(params.index, INGRESS, PRIO_TETHER6, (short) ETH_P_IPV6);
+        } catch (IOException e) {
+            throw new IOException("tc filter del dev (" + params.index + "[" + iface
+                    + "]) ingress prio PRIO_TETHER6 protocol ipv6 failure: " + e);
+        }
+
+        try {
+            // tc filter del dev .. ingress prio 2 protocol ip
+            tcFilterDelDev(params.index, INGRESS, PRIO_TETHER4, (short) ETH_P_IP);
+        } catch (IOException e) {
+            throw new IOException("tc filter del dev (" + params.index + "[" + iface
+                    + "]) ingress prio PRIO_TETHER4 protocol ip failure: " + e);
+        }
+    }
+
+    private static native boolean isEthernet(String iface) throws IOException;
+
+    private static native void tcFilterAddDevBpf(int ifIndex, boolean ingress, short prio,
+            short proto, String bpfProgPath) throws IOException;
+
+    private static native void tcFilterDelDev(int ifIndex, boolean ingress, short prio,
+            short proto) throws IOException;
+}
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index b45db7e..adf1f67 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -247,7 +247,7 @@
             lp.setInterfaceName(upstreamIface);
             dispatchTetherConnectionChanged(upstreamIface, lp, 0);
         }
-        reset(mNetd, mCallback, mAddressCoordinator);
+        reset(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator);
         when(mAddressCoordinator.requestDownstreamAddress(any(), anyBoolean())).thenReturn(
                 mTestAddress);
     }
@@ -471,10 +471,14 @@
         // Telling the state machine about its upstream interface triggers
         // a little more configuration.
         dispatchTetherConnectionChanged(UPSTREAM_IFACE);
-        InOrder inOrder = inOrder(mNetd);
+        InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+
+        // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
+        inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE);
         inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE);
         inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
-        verifyNoMoreInteractions(mNetd, mCallback);
+
+        verifyNoMoreInteractions(mNetd, mCallback, mBpfCoordinator);
     }
 
     @Test
@@ -482,12 +486,19 @@
         initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE);
 
         dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
-        InOrder inOrder = inOrder(mNetd);
+        InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+
+        // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
+        inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
         inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
         inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+
+        // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2>.
+        inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
         inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
         inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
-        verifyNoMoreInteractions(mNetd, mCallback);
+
+        verifyNoMoreInteractions(mNetd, mCallback, mBpfCoordinator);
     }
 
     @Test
@@ -497,10 +508,20 @@
         doThrow(RemoteException.class).when(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
 
         dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
-        InOrder inOrder = inOrder(mNetd);
+        InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+
+        // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
+        inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
         inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
         inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+
+        // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> and expect that failed on
+        // tetherAddForward.
+        inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
         inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
+
+        // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> to fallback.
+        inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE2);
         inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
         inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE2);
     }
@@ -513,11 +534,21 @@
                 IFACE_NAME, UPSTREAM_IFACE2);
 
         dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
-        InOrder inOrder = inOrder(mNetd);
+        InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+
+        // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
+        inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
         inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
         inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+
+        // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> and expect that failed on
+        // ipfwdAddInterfaceForward.
+        inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
         inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
         inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
+
+        // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> to fallback.
+        inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE2);
         inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
         inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE2);
     }
@@ -527,19 +558,22 @@
         initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE);
 
         dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED);
-        InOrder inOrder = inOrder(mNetd, mCallback, mAddressCoordinator);
+        InOrder inOrder = inOrder(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator);
+        inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
         inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
         inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+        inOrder.verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
         inOrder.verify(mNetd).tetherApplyDnsInterfaces();
         inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME);
         inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
         inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
         inOrder.verify(mAddressCoordinator).releaseDownstream(any());
+        inOrder.verify(mBpfCoordinator).stopMonitoring(mIpServer);
         inOrder.verify(mCallback).updateInterfaceState(
                 mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
         inOrder.verify(mCallback).updateLinkProperties(
                 eq(mIpServer), any(LinkProperties.class));
-        verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator);
+        verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator);
     }
 
     @Test
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
index 64ae983..ba4ed47 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -26,9 +26,12 @@
 import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
 import static android.system.OsConstants.ETH_P_IPV6;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker;
 import static com.android.networkstack.tethering.BpfCoordinator.StatsType;
 import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_IFACE;
 import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_UID;
+import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM;
+import static com.android.networkstack.tethering.BpfUtils.UPSTREAM;
 import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
 
 import static org.junit.Assert.assertEquals;
@@ -70,6 +73,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.net.module.util.NetworkStackConstants;
 import com.android.net.module.util.Struct;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
@@ -84,6 +88,7 @@
 import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
 
 import java.net.Inet6Address;
 import java.net.InetAddress;
@@ -1043,6 +1048,59 @@
     }
 
     @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testAttachDetachBpfProgram() throws Exception {
+        setupFunctioningNetdInterface();
+
+        // Static mocking for BpfUtils.
+        MockitoSession mockSession = ExtendedMockito.mockitoSession()
+                .mockStatic(BpfUtils.class)
+                .startMocking();
+        try {
+            final String intIface1 = "wlan1";
+            final String intIface2 = "rndis0";
+            final String extIface = "rmnet_data0";
+            final BpfUtils mockMarkerBpfUtils = staticMockMarker(BpfUtils.class);
+            final BpfCoordinator coordinator = makeBpfCoordinator();
+
+            // [1] Add the forwarding pair <wlan1, rmnet_data0>. Expect that attach both wlan1 and
+            // rmnet_data0.
+            coordinator.maybeAttachProgram(intIface1, extIface);
+            ExtendedMockito.verify(() -> BpfUtils.attachProgram(extIface, DOWNSTREAM));
+            ExtendedMockito.verify(() -> BpfUtils.attachProgram(intIface1, UPSTREAM));
+            ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils);
+            ExtendedMockito.clearInvocations(mockMarkerBpfUtils);
+
+            // [2] Add the forwarding pair <wlan1, rmnet_data0> again. Expect no more action.
+            coordinator.maybeAttachProgram(intIface1, extIface);
+            ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils);
+            ExtendedMockito.clearInvocations(mockMarkerBpfUtils);
+
+            // [3] Add the forwarding pair <rndis0, rmnet_data0>. Expect that attach rndis0 only.
+            coordinator.maybeAttachProgram(intIface2, extIface);
+            ExtendedMockito.verify(() -> BpfUtils.attachProgram(intIface2, UPSTREAM));
+            ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils);
+            ExtendedMockito.clearInvocations(mockMarkerBpfUtils);
+
+            // [4] Remove the forwarding pair <rndis0, rmnet_data0>. Expect detach rndis0 only.
+            coordinator.maybeDetachProgram(intIface2, extIface);
+            ExtendedMockito.verify(() -> BpfUtils.detachProgram(intIface2));
+            ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils);
+            ExtendedMockito.clearInvocations(mockMarkerBpfUtils);
+
+            // [5] Remove the forwarding pair <wlan1, rmnet_data0>. Expect that detach both wlan1
+            // and rmnet_data0.
+            coordinator.maybeDetachProgram(intIface1, extIface);
+            ExtendedMockito.verify(() -> BpfUtils.detachProgram(extIface));
+            ExtendedMockito.verify(() -> BpfUtils.detachProgram(intIface1));
+            ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils);
+            ExtendedMockito.clearInvocations(mockMarkerBpfUtils);
+        } finally {
+            mockSession.finishMocking();
+        }
+    }
+
+    @Test
     public void testTetheringConfigSetPollingInterval() throws Exception {
         setupFunctioningNetdInterface();