Merge "Enable networking tests to be included in partial MTS files as well as full mts."
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 1db4baa..ef96d88 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,9 +1,11 @@
 {
-  // Run in addition to mainline-presubmit as mainline-presubmit is not
-  // supported in every branch.
   "presubmit": [
+    // Run in addition to mainline-presubmit as mainline-presubmit is not
+    // supported in every branch.
+    // CtsNetTestCasesLatestSdk uses stable API shims, so does not exercise
+    // some latest APIs. Run CtsNetTestCases to get coverage of newer APIs.
     {
-      "name": "CtsNetTestCasesLatestSdk",
+      "name": "CtsNetTestCases",
       "options": [
         {
           "exclude-annotation": "com.android.testutils.SkipPresubmit"
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 742fd02..4eafc2a 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -25,7 +25,7 @@
     srcs: [
         "apishim/**/*.java",
         "src/**/*.java",
-        ":framework-tethering-shared-srcs",
+        ":framework-connectivity-shared-srcs",
         ":tethering-module-utils-srcs",
         ":services-tethering-shared-srcs",
     ],
@@ -41,6 +41,7 @@
         "netd-client",
     ],
     libs: [
+        "framework-connectivity",
         "framework-statsd.stubs.module_lib",
         "framework-tethering.impl",
         "framework-wifi",
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index e9deeff..164bda4 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -35,7 +35,10 @@
         "offload.o",
         "test.o",
     ],
-    apps: ["Tethering"],
+    apps: [
+        "ServiceConnectivityResources",
+        "Tethering",
+    ],
     manifest: "manifest.json",
     key: "com.android.tethering.key",
 
@@ -58,6 +61,7 @@
     base: "com.android.tethering",
     package_name: "com.android.tethering.inprocess",
     apps: [
+        "ServiceConnectivityResources",
         "InProcessTethering",
     ],
 }
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 4dc1c51..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.
@@ -84,12 +86,46 @@
 
     public BpfCoordinatorShimImpl(@NonNull final Dependencies deps) {
         mLog = deps.getSharedLog().forSubComponent(TAG);
+
         mBpfDownstream4Map = deps.getBpfDownstream4Map();
         mBpfUpstream4Map = deps.getBpfUpstream4Map();
         mBpfDownstream6Map = deps.getBpfDownstream6Map();
         mBpfUpstream6Map = deps.getBpfUpstream6Map();
         mBpfStatsMap = deps.getBpfStatsMap();
         mBpfLimitMap = deps.getBpfLimitMap();
+
+        // Clear the stubs of the maps for handling the system service crash if any.
+        // Doesn't throw the exception and clear the stubs as many as possible.
+        try {
+            if (mBpfDownstream4Map != null) mBpfDownstream4Map.clear();
+        } catch (ErrnoException e) {
+            mLog.e("Could not clear mBpfDownstream4Map: " + e);
+        }
+        try {
+            if (mBpfUpstream4Map != null) mBpfUpstream4Map.clear();
+        } catch (ErrnoException e) {
+            mLog.e("Could not clear mBpfUpstream4Map: " + e);
+        }
+        try {
+            if (mBpfDownstream6Map != null) mBpfDownstream6Map.clear();
+        } catch (ErrnoException e) {
+            mLog.e("Could not clear mBpfDownstream6Map: " + e);
+        }
+        try {
+            if (mBpfUpstream6Map != null) mBpfUpstream6Map.clear();
+        } catch (ErrnoException e) {
+            mLog.e("Could not clear mBpfUpstream6Map: " + e);
+        }
+        try {
+            if (mBpfStatsMap != null) mBpfStatsMap.clear();
+        } catch (ErrnoException e) {
+            mLog.e("Could not clear mBpfStatsMap: " + e);
+        }
+        try {
+            if (mBpfLimitMap != null) mBpfLimitMap.clear();
+        } catch (ErrnoException e) {
+            mLog.e("Could not clear mBpfLimitMap: " + e);
+        }
     }
 
     @Override
@@ -324,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/bpf_progs/bpf_tethering.h b/Tethering/bpf_progs/bpf_tethering.h
index 6591e81..efda228 100644
--- a/Tethering/bpf_progs/bpf_tethering.h
+++ b/Tethering/bpf_progs/bpf_tethering.h
@@ -16,6 +16,11 @@
 
 #pragma once
 
+#include <linux/if.h>
+#include <linux/if_ether.h>
+#include <linux/in.h>
+#include <linux/in6.h>
+
 // Common definitions for BPF code in the tethering mainline module.
 // These definitions are available to:
 // - The BPF programs in Tethering/bpf_progs/
@@ -59,3 +64,151 @@
     BPF_TETHER_ERRORS
 };
 #undef ERR
+
+// This header file is shared by eBPF kernel programs (C) and netd (C++) and
+// some of the maps are also accessed directly from Java mainline module code.
+//
+// Hence: explicitly pad all relevant structures and assert that their size
+// is the sum of the sizes of their fields.
+#define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.")
+
+
+#define BPF_PATH_TETHER BPF_PATH "tethering/"
+
+#define TETHER_STATS_MAP_PATH BPF_PATH_TETHER "map_offload_tether_stats_map"
+
+typedef uint32_t TetherStatsKey;  // upstream ifindex
+
+typedef struct {
+    uint64_t rxPackets;
+    uint64_t rxBytes;
+    uint64_t rxErrors;
+    uint64_t txPackets;
+    uint64_t txBytes;
+    uint64_t txErrors;
+} TetherStatsValue;
+STRUCT_SIZE(TetherStatsValue, 6 * 8);  // 48
+
+#define TETHER_LIMIT_MAP_PATH BPF_PATH_TETHER "map_offload_tether_limit_map"
+
+typedef uint32_t TetherLimitKey;    // upstream ifindex
+typedef uint64_t TetherLimitValue;  // in bytes
+
+#define TETHER_DOWNSTREAM6_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_downstream6_rawip"
+#define TETHER_DOWNSTREAM6_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_downstream6_ether"
+
+#define TETHER_DOWNSTREAM6_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM6_TC_PROG_RAWIP_NAME
+#define TETHER_DOWNSTREAM6_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM6_TC_PROG_ETHER_NAME
+
+#define TETHER_DOWNSTREAM6_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream6_map"
+
+// For now tethering offload only needs to support downstreams that use 6-byte MAC addresses,
+// because all downstream types that are currently supported (WiFi, USB, Bluetooth and
+// Ethernet) have 6-byte MAC addresses.
+
+typedef struct {
+    uint32_t iif;            // The input interface index
+                             // TODO: extend this to include dstMac
+    struct in6_addr neigh6;  // The destination IPv6 address
+} TetherDownstream6Key;
+STRUCT_SIZE(TetherDownstream6Key, 4 + 16);  // 20
+
+typedef struct {
+    uint32_t oif;             // The output interface to redirect to
+    struct ethhdr macHeader;  // includes dst/src mac and ethertype (zeroed iff rawip egress)
+    uint16_t pmtu;            // The maximum L3 output path/route mtu
+} Tether6Value;
+STRUCT_SIZE(Tether6Value, 4 + 14 + 2);  // 20
+
+#define TETHER_DOWNSTREAM64_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream64_map"
+
+typedef struct {
+    uint32_t iif;              // The input interface index
+    uint8_t dstMac[ETH_ALEN];  // destination ethernet mac address (zeroed iff rawip ingress)
+    uint16_t l4Proto;          // IPPROTO_TCP/UDP/...
+    struct in6_addr src6;      // source &
+    struct in6_addr dst6;      // destination IPv6 addresses
+    __be16 srcPort;            // source &
+    __be16 dstPort;            // destination tcp/udp/... ports
+} TetherDownstream64Key;
+STRUCT_SIZE(TetherDownstream64Key, 4 + 6 + 2 + 16 + 16 + 2 + 2);  // 48
+
+typedef struct {
+    uint32_t oif;             // The output interface to redirect to
+    struct ethhdr macHeader;  // includes dst/src mac and ethertype (zeroed iff rawip egress)
+    uint16_t pmtu;            // The maximum L3 output path/route mtu
+    struct in_addr src4;      // source &
+    struct in_addr dst4;      // destination IPv4 addresses
+    __be16 srcPort;           // source &
+    __be16 outPort;           // destination tcp/udp/... ports
+    uint64_t lastUsed;        // Kernel updates on each use with bpf_ktime_get_boot_ns()
+} TetherDownstream64Value;
+STRUCT_SIZE(TetherDownstream64Value, 4 + 14 + 2 + 4 + 4 + 2 + 2 + 8);  // 40
+
+#define TETHER_UPSTREAM6_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_upstream6_rawip"
+#define TETHER_UPSTREAM6_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_upstream6_ether"
+
+#define TETHER_UPSTREAM6_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM6_TC_PROG_RAWIP_NAME
+#define TETHER_UPSTREAM6_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM6_TC_PROG_ETHER_NAME
+
+#define TETHER_UPSTREAM6_MAP_PATH BPF_PATH_TETHER "map_offload_tether_upstream6_map"
+
+typedef struct {
+    uint32_t iif;  // The input interface index
+                   // TODO: extend this to include dstMac and src ip /64 subnet
+} TetherUpstream6Key;
+STRUCT_SIZE(TetherUpstream6Key, 4);
+
+#define TETHER_DOWNSTREAM4_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_downstream4_rawip"
+#define TETHER_DOWNSTREAM4_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_downstream4_ether"
+
+#define TETHER_DOWNSTREAM4_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM4_TC_PROG_RAWIP_NAME
+#define TETHER_DOWNSTREAM4_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM4_TC_PROG_ETHER_NAME
+
+#define TETHER_DOWNSTREAM4_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream4_map"
+
+
+#define TETHER_UPSTREAM4_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_upstream4_rawip"
+#define TETHER_UPSTREAM4_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_upstream4_ether"
+
+#define TETHER_UPSTREAM4_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM4_TC_PROG_RAWIP_NAME
+#define TETHER_UPSTREAM4_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM4_TC_PROG_ETHER_NAME
+
+#define TETHER_UPSTREAM4_MAP_PATH BPF_PATH_TETHER "map_offload_tether_upstream4_map"
+
+typedef struct {
+    uint32_t iif;              // The input interface index
+    uint8_t dstMac[ETH_ALEN];  // destination ethernet mac address (zeroed iff rawip ingress)
+    uint16_t l4Proto;          // IPPROTO_TCP/UDP/...
+    struct in_addr src4;       // source &
+    struct in_addr dst4;       // destination IPv4 addresses
+    __be16 srcPort;            // source &
+    __be16 dstPort;            // destination TCP/UDP/... ports
+} Tether4Key;
+STRUCT_SIZE(Tether4Key, 4 + 6 + 2 + 4 + 4 + 2 + 2);  // 24
+
+typedef struct {
+    uint32_t oif;             // The output interface to redirect to
+    struct ethhdr macHeader;  // includes dst/src mac and ethertype (zeroed iff rawip egress)
+    uint16_t pmtu;            // Maximum L3 output path/route mtu
+    struct in6_addr src46;    // source &                 (always IPv4 mapped for downstream)
+    struct in6_addr dst46;    // destination IP addresses (may be IPv4 mapped or IPv6 for upstream)
+    __be16 srcPort;           // source &
+    __be16 dstPort;           // destination tcp/udp/... ports
+    uint64_t last_used;       // Kernel updates on each use with bpf_ktime_get_boot_ns()
+} Tether4Value;
+STRUCT_SIZE(Tether4Value, 4 + 14 + 2 + 16 + 16 + 2 + 2 + 8);  // 64
+
+#define TETHER_DOWNSTREAM_XDP_PROG_RAWIP_NAME "prog_offload_xdp_tether_downstream_rawip"
+#define TETHER_DOWNSTREAM_XDP_PROG_ETHER_NAME "prog_offload_xdp_tether_downstream_ether"
+
+#define TETHER_DOWNSTREAM_XDP_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM_XDP_PROG_RAWIP_NAME
+#define TETHER_DOWNSTREAM_XDP_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM_XDP_PROG_ETHER_NAME
+
+#define TETHER_UPSTREAM_XDP_PROG_RAWIP_NAME "prog_offload_xdp_tether_upstream_rawip"
+#define TETHER_UPSTREAM_XDP_PROG_ETHER_NAME "prog_offload_xdp_tether_upstream_ether"
+
+#define TETHER_UPSTREAM_XDP_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM_XDP_PROG_RAWIP_NAME
+#define TETHER_UPSTREAM_XDP_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM_XDP_PROG_ETHER_NAME
+
+#undef STRUCT_SIZE
diff --git a/Tethering/bpf_progs/offload.c b/Tethering/bpf_progs/offload.c
index 16d6ecf..7f9754d 100644
--- a/Tethering/bpf_progs/offload.c
+++ b/Tethering/bpf_progs/offload.c
@@ -27,7 +27,6 @@
 #include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
 #include "bpf_tethering.h"
-#include "netdbpf/bpf_shared.h"
 
 // From kernel:include/net/ip.h
 #define IP_DF 0x4000  // Flag: "Don't Fragment"
@@ -101,7 +100,7 @@
                    AID_NETWORK_STACK)
 
 DEFINE_BPF_MAP_GRW(tether_downstream64_map, HASH, TetherDownstream64Key, TetherDownstream64Value,
-                   64, AID_NETWORK_STACK)
+                   1024, AID_NETWORK_STACK)
 
 DEFINE_BPF_MAP_GRW(tether_upstream6_map, HASH, TetherUpstream6Key, Tether6Value, 64,
                    AID_NETWORK_STACK)
@@ -341,9 +340,9 @@
 
 // ----- IPv4 Support -----
 
-DEFINE_BPF_MAP_GRW(tether_downstream4_map, HASH, Tether4Key, Tether4Value, 64, AID_NETWORK_STACK)
+DEFINE_BPF_MAP_GRW(tether_downstream4_map, HASH, Tether4Key, Tether4Value, 1024, AID_NETWORK_STACK)
 
-DEFINE_BPF_MAP_GRW(tether_upstream4_map, HASH, Tether4Key, Tether4Value, 64, AID_NETWORK_STACK)
+DEFINE_BPF_MAP_GRW(tether_upstream4_map, HASH, Tether4Key, Tether4Value, 1024, AID_NETWORK_STACK)
 
 static inline __always_inline int do_forward4(struct __sk_buff* skb, const bool is_ethernet,
         const bool downstream, const bool updatetime) {
@@ -601,13 +600,7 @@
     return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
 }
 
-// Full featured (required) implementations for 5.8+ kernels
-
-DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
-                     sched_cls_tether_downstream4_ether_5_8, KVER(5, 8, 0))
-(struct __sk_buff* skb) {
-    return do_forward4(skb, /* is_ethernet */ true, /* downstream */ true, /* updatetime */ true);
-}
+// Full featured (required) implementations for 5.8+ kernels (these are S+ by definition)
 
 DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
                      sched_cls_tether_downstream4_rawip_5_8, KVER(5, 8, 0))
@@ -615,28 +608,27 @@
     return do_forward4(skb, /* is_ethernet */ false, /* downstream */ true, /* updatetime */ true);
 }
 
-DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
-                     sched_cls_tether_upstream4_ether_5_8, KVER(5, 8, 0))
-(struct __sk_buff* skb) {
-    return do_forward4(skb, /* is_ethernet */ true, /* downstream */ false, /* updatetime */ true);
-}
-
 DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
                      sched_cls_tether_upstream4_rawip_5_8, KVER(5, 8, 0))
 (struct __sk_buff* skb) {
     return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ true);
 }
 
-// Full featured (optional) implementations for [4.14..5.8) kernels
-
-DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$opt",
-                                    AID_ROOT, AID_NETWORK_STACK,
-                                    sched_cls_tether_downstream4_ether_opt,
-                                    KVER(4, 14, 0), KVER(5, 8, 0))
+DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
+                     sched_cls_tether_downstream4_ether_5_8, KVER(5, 8, 0))
 (struct __sk_buff* skb) {
     return do_forward4(skb, /* is_ethernet */ true, /* downstream */ true, /* updatetime */ true);
 }
 
+DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
+                     sched_cls_tether_upstream4_ether_5_8, KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+    return do_forward4(skb, /* is_ethernet */ true, /* downstream */ false, /* updatetime */ true);
+}
+
+// Full featured (optional) implementations for 4.14-S, 4.19-S & 5.4-S kernels
+// (optional, because we need to be able to fallback for 4.14/4.19/5.4 pre-S kernels)
+
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$opt",
                                     AID_ROOT, AID_NETWORK_STACK,
                                     sched_cls_tether_downstream4_rawip_opt,
@@ -645,14 +637,6 @@
     return do_forward4(skb, /* is_ethernet */ false, /* downstream */ true, /* updatetime */ true);
 }
 
-DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$opt",
-                                    AID_ROOT, AID_NETWORK_STACK,
-                                    sched_cls_tether_upstream4_ether_opt,
-                                    KVER(4, 14, 0), KVER(5, 8, 0))
-(struct __sk_buff* skb) {
-    return do_forward4(skb, /* is_ethernet */ true, /* downstream */ false, /* updatetime */ true);
-}
-
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$opt",
                                     AID_ROOT, AID_NETWORK_STACK,
                                     sched_cls_tether_upstream4_rawip_opt,
@@ -661,8 +645,25 @@
     return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ true);
 }
 
+DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$opt",
+                                    AID_ROOT, AID_NETWORK_STACK,
+                                    sched_cls_tether_downstream4_ether_opt,
+                                    KVER(4, 14, 0), KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+    return do_forward4(skb, /* is_ethernet */ true, /* downstream */ true, /* updatetime */ true);
+}
+
+DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$opt",
+                                    AID_ROOT, AID_NETWORK_STACK,
+                                    sched_cls_tether_upstream4_ether_opt,
+                                    KVER(4, 14, 0), KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+    return do_forward4(skb, /* is_ethernet */ true, /* downstream */ false, /* updatetime */ true);
+}
+
 // Partial (TCP-only: will not update 'last_used' field) implementations for 4.14+ kernels.
-// These will be loaded only if the above optional ones failed (loading of *these* must succeed).
+// These will be loaded only if the above optional ones failed (loading of *these* must succeed
+// for 5.4+, since that is always an R patched kernel).
 //
 // [Note: as a result TCP connections will not have their conntrack timeout refreshed, however,
 // since /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established defaults to 432000 (seconds),
@@ -672,40 +673,73 @@
 // which enforces and documents the required kernel cherrypicks will make it pretty unlikely that
 // many devices upgrading to S will end up relying on these fallback programs.
 
+// RAWIP: Required for 5.4-R kernels -- which always support bpf_skb_change_head().
+
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
+                           sched_cls_tether_downstream4_rawip_5_4, KVER(5, 4, 0), KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+    return do_forward4(skb, /* is_ethernet */ false, /* downstream */ true, /* updatetime */ false);
+}
+
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
+                           sched_cls_tether_upstream4_rawip_5_4, KVER(5, 4, 0), KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+    return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ false);
+}
+
+// RAWIP: Optional for 4.14/4.19 (R) kernels -- which support bpf_skb_change_head().
+// [Note: fallback for 4.14/4.19 (P/Q) kernels is below in stub section]
+
+DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$4_14",
+                                    AID_ROOT, AID_NETWORK_STACK,
+                                    sched_cls_tether_downstream4_rawip_4_14,
+                                    KVER(4, 14, 0), KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+    return do_forward4(skb, /* is_ethernet */ false, /* downstream */ true, /* updatetime */ false);
+}
+
+DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$4_14",
+                                    AID_ROOT, AID_NETWORK_STACK,
+                                    sched_cls_tether_upstream4_rawip_4_14,
+                                    KVER(4, 14, 0), KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+    return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ false);
+}
+
+// ETHER: Required for 4.14-Q/R, 4.19-Q/R & 5.4-R kernels.
+
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_downstream4_ether_4_14, KVER(4, 14, 0), KVER(5, 8, 0))
 (struct __sk_buff* skb) {
     return do_forward4(skb, /* is_ethernet */ true, /* downstream */ true, /* updatetime */ false);
 }
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$4_14", AID_ROOT, AID_NETWORK_STACK,
-                           sched_cls_tether_downstream4_rawip_4_14, KVER(4, 14, 0), KVER(5, 8, 0))
-(struct __sk_buff* skb) {
-    return do_forward4(skb, /* is_ethernet */ false, /* downstream */ true, /* updatetime */ false);
-}
-
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_upstream4_ether_4_14, KVER(4, 14, 0), KVER(5, 8, 0))
 (struct __sk_buff* skb) {
     return do_forward4(skb, /* is_ethernet */ true, /* downstream */ false, /* updatetime */ false);
 }
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$4_14", AID_ROOT, AID_NETWORK_STACK,
-                           sched_cls_tether_upstream4_rawip_4_14, KVER(4, 14, 0), KVER(5, 8, 0))
-(struct __sk_buff* skb) {
-    return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ false);
-}
+// Placeholder (no-op) implementations for older Q kernels
 
-// Placeholder (no-op) implementations for older pre-4.14 kernels
+// RAWIP: 4.9-P/Q, 4.14-P/Q & 4.19-Q kernels -- without bpf_skb_change_head() for tc programs
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$stub", AID_ROOT, AID_NETWORK_STACK,
-                           sched_cls_tether_downstream4_ether_stub, KVER_NONE, KVER(4, 14, 0))
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
+                           sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER(5, 4, 0))
 (struct __sk_buff* skb) {
     return TC_ACT_OK;
 }
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
-                           sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER(4, 14, 0))
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
+                           sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+    return TC_ACT_OK;
+}
+
+// ETHER: 4.9-P/Q kernel
+
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$stub", AID_ROOT, AID_NETWORK_STACK,
+                           sched_cls_tether_downstream4_ether_stub, KVER_NONE, KVER(4, 14, 0))
 (struct __sk_buff* skb) {
     return TC_ACT_OK;
 }
@@ -716,36 +750,75 @@
     return TC_ACT_OK;
 }
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
-                           sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER(4, 14, 0))
-(struct __sk_buff* skb) {
-    return TC_ACT_OK;
+// ----- XDP Support -----
+
+DEFINE_BPF_MAP_GRW(tether_xdp_devmap, DEVMAP_HASH, uint32_t, uint32_t, 64,
+                   AID_NETWORK_STACK)
+
+static inline __always_inline int do_xdp_forward6(struct xdp_md *ctx, const bool is_ethernet,
+        const bool downstream) {
+    return XDP_PASS;
 }
 
-// ----- XDP Support -----
+static inline __always_inline int do_xdp_forward4(struct xdp_md *ctx, const bool is_ethernet,
+        const bool downstream) {
+    return XDP_PASS;
+}
+
+static inline __always_inline int do_xdp_forward_ether(struct xdp_md *ctx, const bool downstream) {
+    const void* data = (void*)(long)ctx->data;
+    const void* data_end = (void*)(long)ctx->data_end;
+    const struct ethhdr* eth = data;
+
+    // Make sure we actually have an ethernet header
+    if ((void*)(eth + 1) > data_end) return XDP_PASS;
+
+    if (eth->h_proto == htons(ETH_P_IPV6))
+        return do_xdp_forward6(ctx, /* is_ethernet */ true, downstream);
+    if (eth->h_proto == htons(ETH_P_IP))
+        return do_xdp_forward4(ctx, /* is_ethernet */ true, downstream);
+
+    // Anything else we don't know how to handle...
+    return XDP_PASS;
+}
+
+static inline __always_inline int do_xdp_forward_rawip(struct xdp_md *ctx, const bool downstream) {
+    const void* data = (void*)(long)ctx->data;
+    const void* data_end = (void*)(long)ctx->data_end;
+
+    // The top nibble of both IPv4 and IPv6 headers is the IP version.
+    if (data_end - data < 1) return XDP_PASS;
+    const uint8_t v = (*(uint8_t*)data) >> 4;
+
+    if (v == 6) return do_xdp_forward6(ctx, /* is_ethernet */ false, downstream);
+    if (v == 4) return do_xdp_forward4(ctx, /* is_ethernet */ false, downstream);
+
+    // Anything else we don't know how to handle...
+    return XDP_PASS;
+}
 
 #define DEFINE_XDP_PROG(str, func) \
     DEFINE_BPF_PROG_KVER(str, AID_ROOT, AID_NETWORK_STACK, func, KVER(5, 9, 0))(struct xdp_md *ctx)
 
 DEFINE_XDP_PROG("xdp/tether_downstream_ether",
                  xdp_tether_downstream_ether) {
-    return XDP_PASS;
+    return do_xdp_forward_ether(ctx, /* downstream */ true);
 }
 
 DEFINE_XDP_PROG("xdp/tether_downstream_rawip",
                  xdp_tether_downstream_rawip) {
-    return XDP_PASS;
+    return do_xdp_forward_rawip(ctx, /* downstream */ true);
 }
 
 DEFINE_XDP_PROG("xdp/tether_upstream_ether",
                  xdp_tether_upstream_ether) {
-    return XDP_PASS;
+    return do_xdp_forward_ether(ctx, /* downstream */ false);
 }
 
 DEFINE_XDP_PROG("xdp/tether_upstream_rawip",
                  xdp_tether_upstream_rawip) {
-    return XDP_PASS;
+    return do_xdp_forward_rawip(ctx, /* downstream */ false);
 }
 
 LICENSE("Apache 2.0");
-CRITICAL("netd");
+CRITICAL("tethering");
diff --git a/Tethering/bpf_progs/test.c b/Tethering/bpf_progs/test.c
index c4a8271..3f0df2e 100644
--- a/Tethering/bpf_progs/test.c
+++ b/Tethering/bpf_progs/test.c
@@ -20,7 +20,7 @@
 
 #include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
-#include "netdbpf/bpf_shared.h"
+#include "bpf_tethering.h"
 
 // Used only by TetheringPrivilegedTests, not by production code.
 DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 2631d08..b141eae 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -26,6 +26,13 @@
     ],
 
     srcs: [":framework-tethering-srcs"],
+    libs: ["framework-connectivity"],
+    stub_only_libs: ["framework-connectivity"],
+    aidl: {
+        include_dirs: [
+            "frameworks/base/packages/Connectivity/framework/aidl-export",
+        ],
+    },
 
     jarjar_rules: "jarjar-rules.txt",
     installable: true,
diff --git a/Tethering/jarjar-rules.txt b/Tethering/jarjar-rules.txt
index d1ad569..5de4b97 100644
--- a/Tethering/jarjar-rules.txt
+++ b/Tethering/jarjar-rules.txt
@@ -1,5 +1,5 @@
-# These must be kept in sync with the framework-tethering-shared-srcs filegroup.
-# Classes from the framework-tethering-shared-srcs filegroup.
+# These must be kept in sync with the framework-connectivity-shared-srcs filegroup.
+# Classes from the framework-connectivity-shared-srcs filegroup.
 # If there are files in that filegroup that are not covered below, the classes in the
 # module will be overwritten by the ones in the framework.
 rule com.android.internal.util.** com.android.networkstack.tethering.util.@1
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 985328f..74eb87b 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,12 +216,16 @@
     // 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();
         maybeSchedulePollingStats();
     };
 
+    // TODO: add BpfMap<TetherDownstream64Key, TetherDownstream64Value> retrieving function.
     @VisibleForTesting
     public abstract static class Dependencies {
         /** Get handler. */
@@ -259,6 +261,7 @@
 
         /** Get downstream4 BPF map. */
         @Nullable public BpfMap<Tether4Key, Tether4Value> getBpfDownstream4Map() {
+            if (!isAtLeastS()) return null;
             try {
                 return new BpfMap<>(TETHER_DOWNSTREAM4_MAP_PATH,
                     BpfMap.BPF_F_RDWR, Tether4Key.class, Tether4Value.class);
@@ -270,6 +273,7 @@
 
         /** Get upstream4 BPF map. */
         @Nullable public BpfMap<Tether4Key, Tether4Value> getBpfUpstream4Map() {
+            if (!isAtLeastS()) return null;
             try {
                 return new BpfMap<>(TETHER_UPSTREAM4_MAP_PATH,
                     BpfMap.BPF_F_RDWR, Tether4Key.class, Tether4Value.class);
@@ -281,6 +285,7 @@
 
         /** Get downstream6 BPF map. */
         @Nullable public BpfMap<TetherDownstream6Key, Tether6Value> getBpfDownstream6Map() {
+            if (!isAtLeastS()) return null;
             try {
                 return new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH,
                     BpfMap.BPF_F_RDWR, TetherDownstream6Key.class, Tether6Value.class);
@@ -292,6 +297,7 @@
 
         /** Get upstream6 BPF map. */
         @Nullable public BpfMap<TetherUpstream6Key, Tether6Value> getBpfUpstream6Map() {
+            if (!isAtLeastS()) return null;
             try {
                 return new BpfMap<>(TETHER_UPSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR,
                         TetherUpstream6Key.class, Tether6Value.class);
@@ -303,6 +309,7 @@
 
         /** Get stats BPF map. */
         @Nullable public BpfMap<TetherStatsKey, TetherStatsValue> getBpfStatsMap() {
+            if (!isAtLeastS()) return null;
             try {
                 return new BpfMap<>(TETHER_STATS_MAP_PATH,
                     BpfMap.BPF_F_RDWR, TetherStatsKey.class, TetherStatsValue.class);
@@ -314,6 +321,7 @@
 
         /** Get limit BPF map. */
         @Nullable public BpfMap<TetherLimitKey, TetherLimitValue> getBpfLimitMap() {
+            if (!isAtLeastS()) return null;
             try {
                 return new BpfMap<>(TETHER_LIMIT_MAP_PATH,
                     BpfMap.BPF_F_RDWR, TetherLimitKey.class, TetherLimitValue.class);
@@ -418,7 +426,8 @@
      * See NetlinkMonitor#handlePacket, NetlinkMessage#parseNfMessage.
      */
     public void startMonitoring(@NonNull final IpServer ipServer) {
-        if (!isUsingBpf()) return;
+        // TODO: Wrap conntrackMonitor starting function into mBpfCoordinatorShim.
+        if (!isUsingBpf() || !mDeps.isAtLeastS()) return;
 
         if (mMonitoringIpServers.contains(ipServer)) {
             Log.wtf(TAG, "The same downstream " + ipServer.interfaceName()
@@ -439,6 +448,9 @@
      * Note that this can be only called on handler thread.
      */
     public void stopMonitoring(@NonNull final IpServer ipServer) {
+        // TODO: Wrap conntrackMonitor stopping function into mBpfCoordinatorShim.
+        if (!isUsingBpf() || !mDeps.isAtLeastS()) return;
+
         mMonitoringIpServers.remove(ipServer);
 
         if (!mMonitoringIpServers.isEmpty()) return;
@@ -690,6 +702,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) {
@@ -795,7 +838,7 @@
             }
             map.forEach((k, v) -> pw.println(ipv6UpstreamRuletoString(k, v)));
         } catch (ErrnoException e) {
-            pw.println("Error dumping IPv4 map: " + e);
+            pw.println("Error dumping IPv6 upstream map: " + e);
         }
     }
 
@@ -842,6 +885,10 @@
     }
 
     private void dumpCounters(@NonNull IndentingPrintWriter pw) {
+        if (!mDeps.isAtLeastS()) {
+            pw.println("No counter support");
+            return;
+        }
         try (BpfMap<U32Struct, U32Struct> map = new BpfMap<>(TETHER_ERROR_MAP_PATH,
                 BpfMap.BPF_F_RDONLY, U32Struct.class, U32Struct.class)) {
 
@@ -1226,6 +1273,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/BpfMap.java b/Tethering/src/com/android/networkstack/tethering/BpfMap.java
index 9a9376f..e9b4ccf 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfMap.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfMap.java
@@ -226,6 +226,20 @@
         closeMap(mMapFd);
     }
 
+    /**
+     * Clears the map. The map may already be empty.
+     *
+     * @throws ErrnoException if the map is already closed, if an error occurred during iteration,
+     *                        or if a non-ENOENT error occurred when deleting a key.
+     */
+    public void clear() throws ErrnoException {
+        K key = getFirstKey();
+        while (key != null) {
+            deleteEntry(key);  // ignores ENOENT.
+            key = getFirstKey();
+        }
+    }
+
     private static native int closeMap(int fd) throws ErrnoException;
 
     private native int bpfFdGet(String path, int mode) throws ErrnoException, NullPointerException;
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/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index 799637c..413b0cb 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -33,6 +33,8 @@
 import android.text.TextUtils;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.DeviceConfigUtils;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -94,6 +96,20 @@
             "tether_enable_select_all_prefix_ranges";
 
     /**
+     * Experiment flag to force choosing upstreams automatically.
+     *
+     * This setting is intended to help force-enable the feature on OEM devices that disabled it
+     * via resource overlays, and later noticed issues. To that end, it overrides
+     * config_tether_upstream_automatic when set to true.
+     *
+     * This flag is enabled if !=0 and less than the module APK version: see
+     * {@link DeviceConfigUtils#isFeatureEnabled}. It is also ignored after R, as later devices
+     * should just set config_tether_upstream_automatic to true instead.
+     */
+    public static final String TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION =
+            "tether_force_upstream_automatic_version";
+
+    /**
      * Default value that used to periodic polls tether offload stats from tethering offload HAL
      * to make the data warnings work.
      */
@@ -146,7 +162,9 @@
 
         isDunRequired = checkDunRequired(ctx);
 
-        chooseUpstreamAutomatically = getResourceBoolean(
+        final boolean forceAutomaticUpstream = !SdkLevel.isAtLeastS()
+                && isFeatureEnabled(ctx, TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION);
+        chooseUpstreamAutomatically = forceAutomaticUpstream || getResourceBoolean(
                 res, R.bool.config_tether_upstream_automatic, false /** defaultValue */);
         preferredUpstreamIfaceTypes = getUpstreamIfaceTypes(res, isDunRequired);
 
@@ -453,6 +471,11 @@
         return DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, name);
     }
 
+    @VisibleForTesting
+    protected boolean isFeatureEnabled(Context ctx, String featureVersionFlag) {
+        return DeviceConfigUtils.isFeatureEnabled(ctx, NAMESPACE_CONNECTIVITY, featureVersionFlag);
+    }
+
     private Resources getResources(Context ctx, int subId) {
         if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
             return getResourcesForSubIdWrapper(ctx, subId);
diff --git a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
new file mode 100644
index 0000000..14dae5c
--- /dev/null
+++ b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2020 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 android.net.ip;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_MTU;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_RDNSS;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_RA_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.net.INetd;
+import android.net.IpPrefix;
+import android.net.MacAddress;
+import android.net.ip.RouterAdvertisementDaemon.RaParams;
+import android.net.util.InterfaceParams;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.EthernetHeader;
+import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv6Header;
+import com.android.net.module.util.structs.LlaOption;
+import com.android.net.module.util.structs.MtuOption;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.net.module.util.structs.RaHeader;
+import com.android.net.module.util.structs.RdnssOption;
+import com.android.testutils.TapPacketReader;
+import com.android.testutils.TapPacketReaderRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.util.HashSet;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class RouterAdvertisementDaemonTest {
+    private static final String TAG = RouterAdvertisementDaemonTest.class.getSimpleName();
+    private static final int DATA_BUFFER_LEN = 4096;
+    private static final int PACKET_TIMEOUT_MS = 5_000;
+
+    @Rule
+    public final TapPacketReaderRule mTetheredReader = new TapPacketReaderRule(
+            DATA_BUFFER_LEN, false /* autoStart */);
+
+    private InterfaceParams mTetheredParams;
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+    private TapPacketReader mTetheredPacketReader;
+    private RouterAdvertisementDaemon mRaDaemon;
+
+    private static INetd sNetd;
+
+    @BeforeClass
+    public static void setupOnce() {
+        System.loadLibrary("tetherutilsjni");
+
+        final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+        final IBinder netdIBinder =
+                (IBinder) inst.getContext().getSystemService(Context.NETD_SERVICE);
+        sNetd = INetd.Stub.asInterface(netdIBinder);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mHandlerThread = new HandlerThread(getClass().getSimpleName());
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+
+        setupTapInterfaces();
+
+        // Looper must be prepared here since AndroidJUnitRunner runs tests on separate threads.
+        if (Looper.myLooper() == null) Looper.prepare();
+
+        mRaDaemon = new RouterAdvertisementDaemon(mTetheredParams);
+        sNetd.networkAddInterface(INetd.LOCAL_NET_ID, mTetheredParams.name);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mTetheredReader.stop();
+        if (mHandlerThread != null) {
+            mHandlerThread.quitSafely();
+            mHandlerThread.join(PACKET_TIMEOUT_MS);
+        }
+
+        if (mTetheredParams != null) {
+            sNetd.networkRemoveInterface(INetd.LOCAL_NET_ID, mTetheredParams.name);
+        }
+    }
+
+    private void setupTapInterfaces() {
+        // Create tethered test iface.
+        mTetheredReader.start(mHandler);
+        mTetheredParams = InterfaceParams.getByName(mTetheredReader.iface.getInterfaceName());
+        assertNotNull(mTetheredParams);
+        mTetheredPacketReader = mTetheredReader.getReader();
+        mHandler.post(mTetheredPacketReader::start);
+    }
+
+    private class TestRaPacket {
+        final RaParams mNewParams, mOldParams;
+
+        TestRaPacket(final RaParams oldParams, final RaParams newParams) {
+            mOldParams = oldParams;
+            mNewParams = newParams;
+        }
+
+        public boolean isPacketMatched(final byte[] pkt) throws Exception {
+            if (pkt.length < (ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_RA_HEADER_LEN)) {
+                return false;
+            }
+            final ByteBuffer buf = ByteBuffer.wrap(pkt);
+
+            // Parse Ethernet header
+            final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf);
+            if (ethHdr.etherType != ETHER_TYPE_IPV6) return false;
+
+            // Parse IPv6 header
+            final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf);
+            assertEquals((ipv6Hdr.vtf >> 28), 6 /* ip version*/);
+
+            final int payLoadLength = pkt.length - ETHER_HEADER_LEN - IPV6_HEADER_LEN;
+            assertEquals(payLoadLength, ipv6Hdr.payloadLength);
+
+            // Parse ICMPv6 header
+            final Icmpv6Header icmpv6Hdr = Struct.parse(Icmpv6Header.class, buf);
+            if (icmpv6Hdr.type != (short) ICMPV6_ROUTER_ADVERTISEMENT) return false;
+
+            // Parse RA header
+            final RaHeader raHdr = Struct.parse(RaHeader.class, buf);
+            assertEquals(mNewParams.hopLimit, raHdr.hopLimit);
+
+            while (buf.position() < pkt.length) {
+                final int currentPos = buf.position();
+                final int type = Byte.toUnsignedInt(buf.get());
+                final int length = Byte.toUnsignedInt(buf.get());
+                switch (type) {
+                    case ICMPV6_ND_OPTION_PIO:
+                        assertEquals(4, length);
+
+                        final ByteBuffer pioBuf = ByteBuffer.wrap(buf.array(), currentPos,
+                                Struct.getSize(PrefixInformationOption.class));
+                        final PrefixInformationOption pio =
+                                Struct.parse(PrefixInformationOption.class, pioBuf);
+                        assertEquals((byte) 0xc0, pio.flags); // L & A set
+
+                        final InetAddress address = InetAddress.getByAddress(pio.prefix);
+                        final IpPrefix prefix = new IpPrefix(address, pio.prefixLen);
+                        if (mNewParams.prefixes.contains(prefix)) {
+                            assertTrue(pio.validLifetime > 0);
+                            assertTrue(pio.preferredLifetime > 0);
+                        } else if (mOldParams != null && mOldParams.prefixes.contains(prefix)) {
+                            assertEquals(0, pio.validLifetime);
+                            assertEquals(0, pio.preferredLifetime);
+                        } else {
+                            fail("Unepxected prefix: " + prefix);
+                        }
+
+                        // Move ByteBuffer position to the next option.
+                        buf.position(currentPos + Struct.getSize(PrefixInformationOption.class));
+                        break;
+                    case ICMPV6_ND_OPTION_MTU:
+                        assertEquals(1, length);
+
+                        final ByteBuffer mtuBuf = ByteBuffer.wrap(buf.array(), currentPos,
+                                Struct.getSize(MtuOption.class));
+                        final MtuOption mtu = Struct.parse(MtuOption.class, mtuBuf);
+                        assertEquals(mNewParams.mtu, mtu.mtu);
+
+                        // Move ByteBuffer position to the next option.
+                        buf.position(currentPos + Struct.getSize(MtuOption.class));
+                        break;
+                    case ICMPV6_ND_OPTION_RDNSS:
+                        final int rdnssHeaderLen = Struct.getSize(RdnssOption.class);
+                        final ByteBuffer RdnssBuf = ByteBuffer.wrap(buf.array(), currentPos,
+                                rdnssHeaderLen);
+                        final RdnssOption rdnss = Struct.parse(RdnssOption.class, RdnssBuf);
+                        final String msg =
+                                rdnss.lifetime > 0 ? "Unknown dns" : "Unknown deprecated dns";
+                        final HashSet<Inet6Address> dnses =
+                                rdnss.lifetime > 0 ? mNewParams.dnses : mOldParams.dnses;
+                        assertNotNull(msg, dnses);
+
+                        // Check DNS servers included in this option.
+                        buf.position(currentPos + rdnssHeaderLen); // skip the rdnss option header
+                        final int numOfDnses = (length - 1) / 2;
+                        for (int i = 0; i < numOfDnses; i++) {
+                            byte[] rawAddress = new byte[IPV6_ADDR_LEN];
+                            buf.get(rawAddress);
+                            final Inet6Address dns =
+                                    (Inet6Address) InetAddress.getByAddress(rawAddress);
+                            if (!dnses.contains(dns)) fail("Unexpected dns: " + dns);
+                        }
+                        // Unnecessary to move ByteBuffer position here, since the position has been
+                        // moved forward correctly after reading DNS servers from ByteBuffer.
+                        break;
+                    case ICMPV6_ND_OPTION_SLLA:
+                        // Do nothing, just move ByteBuffer position to the next option.
+                        buf.position(currentPos + Struct.getSize(LlaOption.class));
+                        break;
+                    default:
+                        fail("Unknown RA option type " + type);
+                }
+            }
+            return true;
+        }
+    }
+
+    private RaParams createRaParams(final String ipv6Address) throws Exception {
+        final RaParams params = new RaParams();
+        final Inet6Address address = (Inet6Address) InetAddress.getByName(ipv6Address);
+        params.dnses.add(address);
+        params.prefixes.add(new IpPrefix(address, 64));
+
+        return params;
+    }
+
+    private boolean assertRaPacket(final TestRaPacket testRa)
+            throws Exception {
+        byte[] packet;
+        while ((packet = mTetheredPacketReader.poll(PACKET_TIMEOUT_MS)) != null) {
+            if (testRa.isPacketMatched(packet)) return true;
+        }
+        return false;
+    }
+
+    private ByteBuffer createRsPacket(final String srcIp) throws Exception {
+        final MacAddress dstMac = MacAddress.fromString("33:33:03:04:05:06");
+        final MacAddress srcMac = mTetheredParams.macAddr;
+        final ByteBuffer slla = LlaOption.build((byte) ICMPV6_ND_OPTION_SLLA, srcMac);
+
+        return Ipv6Utils.buildRsPacket(srcMac, dstMac, (Inet6Address) InetAddress.getByName(srcIp),
+                IPV6_ADDR_ALL_NODES_MULTICAST, slla);
+    }
+
+    @Test
+    public void testUnSolicitRouterAdvertisement() throws Exception {
+        assertTrue(mRaDaemon.start());
+        final RaParams params1 = createRaParams("2001:1122:3344::5566");
+        mRaDaemon.buildNewRa(null, params1);
+        assertRaPacket(new TestRaPacket(null, params1));
+
+        final RaParams params2 = createRaParams("2006:3344:5566::7788");
+        mRaDaemon.buildNewRa(params1, params2);
+        assertRaPacket(new TestRaPacket(params1, params2));
+    }
+
+    @Test
+    public void testSolicitRouterAdvertisement() throws Exception {
+        assertTrue(mRaDaemon.start());
+        final RaParams params1 = createRaParams("2001:1122:3344::5566");
+        mRaDaemon.buildNewRa(null, params1);
+        assertRaPacket(new TestRaPacket(null, params1));
+
+        final ByteBuffer rs = createRsPacket("fe80::1122:3344:5566:7788");
+        mTetheredPacketReader.sendResponse(rs);
+        assertRaPacket(new TestRaPacket(null, params1));
+    }
+}
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
index cceaa8c..62302c3 100644
--- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
@@ -312,6 +312,32 @@
     }
 
     @Test
+    public void testClear() throws Exception {
+        // Clear an empty map.
+        assertTrue(mTestMap.isEmpty());
+        mTestMap.clear();
+
+        // Clear a map with some data in it.
+        final ArrayMap<TetherDownstream6Key, Tether6Value> resultMap =
+                new ArrayMap<>(mTestData);
+        for (int i = 0; i < resultMap.size(); i++) {
+            mTestMap.insertEntry(resultMap.keyAt(i), resultMap.valueAt(i));
+        }
+        assertFalse(mTestMap.isEmpty());
+        mTestMap.clear();
+        assertTrue(mTestMap.isEmpty());
+
+        // Clearing an already-closed map throws.
+        mTestMap.close();
+        try {
+            mTestMap.clear();
+            fail("clearing already-closed map should throw");
+        } catch (ErrnoException expected) {
+            assertEquals(OsConstants.EBADF, expected.errno);
+        }
+    }
+
+    @Test
     public void testInsertOverflow() throws Exception {
         final ArrayMap<TetherDownstream6Key, Tether6Value> testData =
                 new ArrayMap<>();
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/android/net/util/TetheringUtilsTest.java b/Tethering/tests/unit/src/android/net/util/TetheringUtilsTest.java
index 91c7771..9968b5f 100644
--- a/Tethering/tests/unit/src/android/net/util/TetheringUtilsTest.java
+++ b/Tethering/tests/unit/src/android/net/util/TetheringUtilsTest.java
@@ -17,27 +17,49 @@
 
 import static android.net.TetheringManager.TETHERING_USB;
 import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.system.OsConstants.AF_UNIX;
+import static android.system.OsConstants.EAGAIN;
+import static android.system.OsConstants.SOCK_CLOEXEC;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_NONBLOCK;
 
+import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
 import android.net.LinkAddress;
+import android.net.MacAddress;
 import android.net.TetheringRequestParcel;
+import android.system.ErrnoException;
+import android.system.Os;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.EthernetHeader;
+import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv6Header;
 import com.android.testutils.MiscAsserts;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.FileDescriptor;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class TetheringUtilsTest {
     private static final LinkAddress TEST_SERVER_ADDR = new LinkAddress("192.168.43.1/24");
     private static final LinkAddress TEST_CLIENT_ADDR = new LinkAddress("192.168.43.5/24");
+    private static final int PACKET_SIZE = 1500;
+
     private TetheringRequestParcel mTetheringRequest;
 
     @Before
@@ -84,4 +106,82 @@
 
         MiscAsserts.assertFieldCountEquals(5, TetheringRequestParcel.class);
     }
+
+    // Writes the specified packet to a filedescriptor, skipping the Ethernet header.
+    // Needed because the Ipv6Utils methods for building packets always include the Ethernet header,
+    // but socket filters applied by TetheringUtils expect the packet to start from the IP header.
+    private int writePacket(FileDescriptor fd, ByteBuffer pkt) throws Exception {
+        pkt.flip();
+        int offset = Struct.getSize(EthernetHeader.class);
+        int len = pkt.capacity() - offset;
+        return Os.write(fd, pkt.array(), offset, len);
+    }
+
+    // Reads a packet from the filedescriptor.
+    private ByteBuffer readIpPacket(FileDescriptor fd) throws Exception {
+        ByteBuffer buf = ByteBuffer.allocate(PACKET_SIZE);
+        Os.read(fd, buf);
+        return buf;
+    }
+
+    private interface SocketFilter {
+        void apply(FileDescriptor fd) throws Exception;
+    }
+
+    private ByteBuffer checkIcmpSocketFilter(ByteBuffer passed, ByteBuffer dropped,
+            SocketFilter filter) throws Exception {
+        FileDescriptor in = new FileDescriptor();
+        FileDescriptor out = new FileDescriptor();
+        Os.socketpair(AF_UNIX, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, in, out);
+
+        // Before the filter is applied, it doesn't drop anything.
+        int len = writePacket(out, dropped);
+        ByteBuffer received = readIpPacket(in);
+        assertEquals(len, received.position());
+
+        // Install the socket filter. Then write two packets, the first expected to be dropped and
+        // the second expected to be passed. Check that only the second makes it through.
+        filter.apply(in);
+        writePacket(out, dropped);
+        len = writePacket(out, passed);
+        received = readIpPacket(in);
+        assertEquals(len, received.position());
+        received.flip();
+
+        // Check there are no more packets to read.
+        try {
+            readIpPacket(in);
+        } catch (ErrnoException expected) {
+            assertEquals(EAGAIN, expected.errno);
+        }
+
+        return received;
+    }
+
+    @Test
+    public void testIcmpSocketFilters() throws Exception {
+        MacAddress mac1 = MacAddress.fromString("11:22:33:44:55:66");
+        MacAddress mac2 = MacAddress.fromString("aa:bb:cc:dd:ee:ff");
+        Inet6Address ll1 = (Inet6Address) InetAddress.getByName("fe80::1");
+        Inet6Address ll2 = (Inet6Address) InetAddress.getByName("fe80::abcd");
+        Inet6Address allRouters = NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
+
+        final ByteBuffer na = Ipv6Utils.buildNaPacket(mac1, mac2, ll1, ll2, 0, ll1);
+        final ByteBuffer ns = Ipv6Utils.buildNsPacket(mac1, mac2, ll1, ll2, ll1);
+        final ByteBuffer rs = Ipv6Utils.buildRsPacket(mac1, mac2, ll1, allRouters);
+
+        ByteBuffer received = checkIcmpSocketFilter(na /* passed */, rs /* dropped */,
+                TetheringUtils::setupNaSocket);
+
+        Struct.parse(Ipv6Header.class, received);  // Skip IPv6 header.
+        Icmpv6Header icmpv6 = Struct.parse(Icmpv6Header.class, received);
+        assertEquals(NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT, icmpv6.type);
+
+        received = checkIcmpSocketFilter(ns /* passed */, rs /* dropped */,
+                TetheringUtils::setupNsSocket);
+
+        Struct.parse(Ipv6Header.class, received);  // Skip IPv6 header.
+        icmpv6 = Struct.parse(Icmpv6Header.class, received);
+        assertEquals(NetworkStackConstants.ICMPV6_NEIGHBOR_SOLICITATION, icmpv6.type);
+    }
 }
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 1270e50..293d0df 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,13 +73,17 @@
 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;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.TestableNetworkStatsProviderCbBinder;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -84,6 +91,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;
@@ -97,6 +105,9 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class BpfCoordinatorTest {
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
     private static final int DOWNSTREAM_IFINDEX = 10;
     private static final MacAddress DOWNSTREAM_MAC = MacAddress.ALL_ZEROS_ADDRESS;
     private static final InetAddress NEIGH_A = InetAddresses.parseNumericAddress("2001:db8::1");
@@ -150,6 +161,12 @@
             // BpfMap#getValue treats that the entry is not found as no error.
             return mMap.get(key);
         }
+
+        @Override
+        public void clear() throws ErrnoException {
+            // TODO: consider using mocked #getFirstKey and #deleteEntry to implement.
+            mMap.clear();
+        }
     };
 
     @Mock private NetworkStatsManager mStatsManager;
@@ -988,6 +1005,24 @@
 
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testBpfDisabledbyNoBpfDownstream4Map() throws Exception {
+        setupFunctioningNetdInterface();
+        doReturn(null).when(mDeps).getBpfDownstream4Map();
+
+        checkBpfDisabled();
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testBpfDisabledbyNoBpfUpstream4Map() throws Exception {
+        setupFunctioningNetdInterface();
+        doReturn(null).when(mDeps).getBpfUpstream4Map();
+
+        checkBpfDisabled();
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
     public void testBpfDisabledbyNoBpfStatsMap() throws Exception {
         setupFunctioningNetdInterface();
         doReturn(null).when(mDeps).getBpfStatsMap();
@@ -1005,6 +1040,73 @@
     }
 
     @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testBpfMapClear() throws Exception {
+        setupFunctioningNetdInterface();
+
+        final BpfCoordinator coordinator = makeBpfCoordinator();
+        verify(mBpfDownstream4Map).clear();
+        verify(mBpfUpstream4Map).clear();
+        verify(mBpfDownstream6Map).clear();
+        verify(mBpfUpstream6Map).clear();
+        verify(mBpfStatsMap).clear();
+        verify(mBpfLimitMap).clear();
+    }
+
+    @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();
 
@@ -1054,6 +1156,7 @@
     }
 
     @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
     public void testStartStopConntrackMonitoring() throws Exception {
         setupFunctioningNetdInterface();
 
@@ -1074,6 +1177,23 @@
     }
 
     @Test
+    @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @IgnoreAfter(Build.VERSION_CODES.R)
+    // Only run this test on Android R.
+    public void testStartStopConntrackMonitoring_R() throws Exception {
+        setupFunctioningNetdInterface();
+
+        final BpfCoordinator coordinator = makeBpfCoordinator();
+
+        coordinator.startMonitoring(mIpServer);
+        verify(mConntrackMonitor, never()).start();
+
+        coordinator.stopMonitoring(mIpServer);
+        verify(mConntrackMonitor, never()).stop();
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
     public void testStartStopConntrackMonitoringWithTwoDownstreamIfaces() throws Exception {
         setupFunctioningNetdInterface();
 
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
index 354e753..8cfa7d0 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -53,6 +53,8 @@
 
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.net.util.SharedLog;
 import android.os.Bundle;
@@ -87,11 +89,13 @@
     private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
     private static final String PROVISIONING_NO_UI_APP_NAME = "no_ui_app";
     private static final String PROVISIONING_APP_RESPONSE = "app_response";
+    private static final String TEST_PACKAGE_NAME = "com.android.tethering.test";
 
     @Mock private CarrierConfigManager mCarrierConfigManager;
     @Mock private Context mContext;
     @Mock private Resources mResources;
     @Mock private SharedLog mLog;
+    @Mock private PackageManager mPm;
     @Mock private EntitlementManager.OnUiEntitlementFailedListener mEntitlementFailedListener;
 
     // Like so many Android system APIs, these cannot be mocked because it is marked final.
@@ -182,7 +186,7 @@
     }
 
     @Before
-    public void setUp() {
+    public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         mMockingSession = mockitoSession()
                 .initMocks(this)
@@ -196,6 +200,9 @@
                 eq(EntitlementManager.DISABLE_PROVISIONING_SYSPROP_KEY), anyBoolean()));
         doReturn(null).when(
                 () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY), anyString()));
+        doReturn(mPm).when(mContext).getPackageManager();
+        doReturn(TEST_PACKAGE_NAME).when(mContext).getPackageName();
+        doReturn(new PackageInfo()).when(mPm).getPackageInfo(anyString(), anyInt());
 
         when(mResources.getStringArray(R.array.config_tether_dhcp_range))
                 .thenReturn(new String[0]);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
new file mode 100644
index 0000000..3636b03
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
@@ -0,0 +1,323 @@
+/*
+ * 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 org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.IConnectivityManager;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkRequest;
+import android.os.Handler;
+import android.os.UserHandle;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Simulates upstream switching and sending NetworkCallbacks and CONNECTIVITY_ACTION broadcasts.
+ *
+ * Unlike any real networking code, this class is single-threaded and entirely synchronous.
+ * The effects of all method calls (including sending fake broadcasts, sending callbacks, etc.) are
+ * performed immediately on the caller's thread before returning.
+ *
+ * TODO: this duplicates a fair amount of code from ConnectivityManager and ConnectivityService.
+ * Consider using a ConnectivityService object instead, as used in ConnectivityServiceTest.
+ *
+ * Things to consider:
+ * - ConnectivityService uses a real handler for realism, and these test use TestLooper (or even
+ *   invoke callbacks directly inline) for determinism. Using a real ConnectivityService would
+ *   require adding dispatchAll() calls and migrating to handlers.
+ * - ConnectivityService does not provide a way to order CONNECTIVITY_ACTION before or after the
+ *   NetworkCallbacks for the same network change. That ability is useful because the upstream
+ *   selection code in Tethering is vulnerable to race conditions, due to its reliance on multiple
+ *   separate NetworkCallbacks and BroadcastReceivers, each of which trigger different types of
+ *   updates. If/when the upstream selection code is refactored to a more level-triggered model
+ *   (e.g., with an idempotent function that takes into account all state every time any part of
+ *   that state changes), this may become less important or unnecessary.
+ */
+public class TestConnectivityManager extends ConnectivityManager {
+    public Map<NetworkCallback, Handler> allCallbacks = new HashMap<>();
+    public Set<NetworkCallback> trackingDefault = new HashSet<>();
+    public TestNetworkAgent defaultNetwork = null;
+    public Map<NetworkCallback, NetworkRequest> listening = new HashMap<>();
+    public Map<NetworkCallback, NetworkRequest> requested = new HashMap<>();
+    public Map<NetworkCallback, Integer> legacyTypeMap = new HashMap<>();
+
+    private final NetworkRequest mDefaultRequest;
+    private final Context mContext;
+
+    private int mNetworkId = 100;
+
+    /**
+     * Constructs a TestConnectivityManager.
+     * @param ctx the context to use. Must be a fake or a mock because otherwise the test will
+     *            attempt to send real broadcasts and resulting in permission denials.
+     * @param svc an IConnectivityManager. Should be a fake or a mock.
+     * @param defaultRequest the default NetworkRequest that will be used by Tethering.
+     */
+    public TestConnectivityManager(Context ctx, IConnectivityManager svc,
+            NetworkRequest defaultRequest) {
+        super(ctx, svc);
+        mContext = ctx;
+        mDefaultRequest = defaultRequest;
+    }
+
+    boolean hasNoCallbacks() {
+        return allCallbacks.isEmpty()
+                && trackingDefault.isEmpty()
+                && listening.isEmpty()
+                && requested.isEmpty()
+                && legacyTypeMap.isEmpty();
+    }
+
+    boolean onlyHasDefaultCallbacks() {
+        return (allCallbacks.size() == 1)
+                && (trackingDefault.size() == 1)
+                && listening.isEmpty()
+                && requested.isEmpty()
+                && legacyTypeMap.isEmpty();
+    }
+
+    boolean isListeningForAll() {
+        final NetworkCapabilities empty = new NetworkCapabilities();
+        empty.clearAll();
+
+        for (NetworkRequest req : listening.values()) {
+            if (req.networkCapabilities.equalRequestableCapabilities(empty)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    int getNetworkId() {
+        return ++mNetworkId;
+    }
+
+    void makeDefaultNetwork(TestNetworkAgent agent) {
+        if (Objects.equals(defaultNetwork, agent)) return;
+
+        final TestNetworkAgent formerDefault = defaultNetwork;
+        defaultNetwork = agent;
+
+        if (formerDefault != null) {
+            sendConnectivityAction(formerDefault.legacyType, false /* connected */);
+        }
+        if (defaultNetwork != null) {
+            sendConnectivityAction(defaultNetwork.legacyType, true /* connected */);
+        }
+
+        for (NetworkCallback cb : trackingDefault) {
+            if (defaultNetwork != null) {
+                cb.onAvailable(defaultNetwork.networkId);
+                cb.onCapabilitiesChanged(
+                        defaultNetwork.networkId, defaultNetwork.networkCapabilities);
+                cb.onLinkPropertiesChanged(
+                        defaultNetwork.networkId, defaultNetwork.linkProperties);
+            }
+        }
+    }
+
+    @Override
+    public void requestNetwork(NetworkRequest req, NetworkCallback cb, Handler h) {
+        assertFalse(allCallbacks.containsKey(cb));
+        allCallbacks.put(cb, h);
+        if (mDefaultRequest.equals(req)) {
+            assertFalse(trackingDefault.contains(cb));
+            trackingDefault.add(cb);
+        } else {
+            assertFalse(requested.containsKey(cb));
+            requested.put(cb, req);
+        }
+    }
+
+    @Override
+    public void requestNetwork(NetworkRequest req, NetworkCallback cb) {
+        fail("Should never be called.");
+    }
+
+    @Override
+    public void requestNetwork(NetworkRequest req,
+            int timeoutMs, int legacyType, Handler h, NetworkCallback cb) {
+        assertFalse(allCallbacks.containsKey(cb));
+        allCallbacks.put(cb, h);
+        assertFalse(requested.containsKey(cb));
+        requested.put(cb, req);
+        assertFalse(legacyTypeMap.containsKey(cb));
+        if (legacyType != ConnectivityManager.TYPE_NONE) {
+            legacyTypeMap.put(cb, legacyType);
+        }
+    }
+
+    @Override
+    public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb, Handler h) {
+        assertFalse(allCallbacks.containsKey(cb));
+        allCallbacks.put(cb, h);
+        assertFalse(listening.containsKey(cb));
+        listening.put(cb, req);
+    }
+
+    @Override
+    public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb) {
+        fail("Should never be called.");
+    }
+
+    @Override
+    public void registerDefaultNetworkCallback(NetworkCallback cb, Handler h) {
+        fail("Should never be called.");
+    }
+
+    @Override
+    public void registerDefaultNetworkCallback(NetworkCallback cb) {
+        fail("Should never be called.");
+    }
+
+    @Override
+    public void unregisterNetworkCallback(NetworkCallback cb) {
+        if (trackingDefault.contains(cb)) {
+            trackingDefault.remove(cb);
+        } else if (listening.containsKey(cb)) {
+            listening.remove(cb);
+        } else if (requested.containsKey(cb)) {
+            requested.remove(cb);
+            legacyTypeMap.remove(cb);
+        } else {
+            fail("Unexpected callback removed");
+        }
+        allCallbacks.remove(cb);
+
+        assertFalse(allCallbacks.containsKey(cb));
+        assertFalse(trackingDefault.contains(cb));
+        assertFalse(listening.containsKey(cb));
+        assertFalse(requested.containsKey(cb));
+    }
+
+    private void sendConnectivityAction(int type, boolean connected) {
+        NetworkInfo ni = new NetworkInfo(type, 0 /* subtype */,  getNetworkTypeName(type),
+                "" /* subtypeName */);
+        NetworkInfo.DetailedState state = connected
+                ? NetworkInfo.DetailedState.CONNECTED
+                : NetworkInfo.DetailedState.DISCONNECTED;
+        ni.setDetailedState(state, "" /* reason */, "" /* extraInfo */);
+        Intent intent = new Intent(CONNECTIVITY_ACTION);
+        intent.putExtra(EXTRA_NETWORK_INFO, ni);
+        mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+    }
+
+    public static class TestNetworkAgent {
+        public final TestConnectivityManager cm;
+        public final Network networkId;
+        public final NetworkCapabilities networkCapabilities;
+        public final LinkProperties linkProperties;
+        // TODO: delete when tethering no longer uses CONNECTIVITY_ACTION.
+        public final int legacyType;
+
+        public TestNetworkAgent(TestConnectivityManager cm, NetworkCapabilities nc) {
+            this.cm = cm;
+            this.networkId = new Network(cm.getNetworkId());
+            networkCapabilities = copy(nc);
+            linkProperties = new LinkProperties();
+            legacyType = toLegacyType(nc);
+        }
+
+        public TestNetworkAgent(TestConnectivityManager cm, UpstreamNetworkState state) {
+            this.cm = cm;
+            networkId = state.network;
+            networkCapabilities = state.networkCapabilities;
+            linkProperties = state.linkProperties;
+            this.legacyType = toLegacyType(networkCapabilities);
+        }
+
+        private static int toLegacyType(NetworkCapabilities nc) {
+            for (int type = 0; type < ConnectivityManager.TYPE_TEST; type++) {
+                if (matchesLegacyType(nc, type)) return type;
+            }
+            throw new IllegalArgumentException(("Can't determine legacy type for: ") + nc);
+        }
+
+        private static boolean matchesLegacyType(NetworkCapabilities nc, int legacyType) {
+            final NetworkCapabilities typeNc;
+            try {
+                typeNc = ConnectivityManager.networkCapabilitiesForType(legacyType);
+            } catch (IllegalArgumentException e) {
+                // networkCapabilitiesForType does not support all legacy types.
+                return false;
+            }
+            return typeNc.satisfiedByNetworkCapabilities(nc);
+        }
+
+        private boolean matchesLegacyType(int legacyType) {
+            return matchesLegacyType(networkCapabilities, legacyType);
+        }
+
+        public void fakeConnect() {
+            for (NetworkRequest request : cm.requested.values()) {
+                if (matchesLegacyType(request.legacyType)) {
+                    cm.sendConnectivityAction(legacyType, true /* connected */);
+                    // In practice, a given network can match only one legacy type.
+                    break;
+                }
+            }
+            for (NetworkCallback cb : cm.listening.keySet()) {
+                cb.onAvailable(networkId);
+                cb.onCapabilitiesChanged(networkId, copy(networkCapabilities));
+                cb.onLinkPropertiesChanged(networkId, copy(linkProperties));
+            }
+        }
+
+        public void fakeDisconnect() {
+            for (NetworkRequest request : cm.requested.values()) {
+                if (matchesLegacyType(request.legacyType)) {
+                    cm.sendConnectivityAction(legacyType, false /* connected */);
+                    break;
+                }
+            }
+            for (NetworkCallback cb : cm.listening.keySet()) {
+                cb.onLost(networkId);
+            }
+        }
+
+        public void sendLinkProperties() {
+            for (NetworkCallback cb : cm.listening.keySet()) {
+                cb.onLinkPropertiesChanged(networkId, copy(linkProperties));
+            }
+        }
+
+        @Override
+        public String toString() {
+            return String.format("TestNetworkAgent: %s %s", networkId, networkCapabilities);
+        }
+    }
+
+    static NetworkCapabilities copy(NetworkCapabilities nc) {
+        return new NetworkCapabilities(nc);
+    }
+
+    static LinkProperties copy(LinkProperties lp) {
+        return new LinkProperties(lp);
+    }
+}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
index 237e2c2..1f4e371 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
@@ -30,12 +30,16 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.net.util.SharedLog;
+import android.os.Build;
 import android.provider.DeviceConfig;
 import android.telephony.TelephonyManager;
 
@@ -43,9 +47,14 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.net.module.util.DeviceConfigUtils;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -60,13 +69,18 @@
 public class TetheringConfigurationTest {
     private final SharedLog mLog = new SharedLog("TetheringConfigurationTest");
 
+    @Rule public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
     private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
     private static final String PROVISIONING_NO_UI_APP_NAME = "no_ui_app";
     private static final String PROVISIONING_APP_RESPONSE = "app_response";
+    private static final String TEST_PACKAGE_NAME = "com.android.tethering.test";
+    private static final long TEST_PACKAGE_VERSION = 1234L;
     @Mock private Context mContext;
     @Mock private TelephonyManager mTelephonyManager;
     @Mock private Resources mResources;
     @Mock private Resources mResourcesForSubId;
+    @Mock private PackageManager mPackageManager;
     private Context mMockContext;
     private boolean mHasTelephonyManager;
     private boolean mEnableLegacyDhcpServer;
@@ -100,6 +114,16 @@
             }
             return super.getSystemService(name);
         }
+
+        @Override
+        public PackageManager getPackageManager() {
+            return mPackageManager;
+        }
+
+        @Override
+        public String getPackageName() {
+            return TEST_PACKAGE_NAME;
+        }
     }
 
     @Before
@@ -110,9 +134,15 @@
                 .mockStatic(DeviceConfig.class)
                 .strictness(Strictness.WARN)
                 .startMocking();
+        DeviceConfigUtils.resetPackageVersionCacheForTest();
         doReturn(null).when(
                 () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY),
                 eq(TetheringConfiguration.TETHER_ENABLE_LEGACY_DHCP_SERVER)));
+        setTetherForceUpstreamAutomaticFlagVersion(null);
+
+        final PackageInfo pi = new PackageInfo();
+        pi.setLongVersionCode(TEST_PACKAGE_VERSION);
+        doReturn(pi).when(mPackageManager).getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt());
 
         when(mResources.getStringArray(R.array.config_tether_dhcp_range)).thenReturn(
                 new String[0]);
@@ -141,6 +171,7 @@
     @After
     public void tearDown() throws Exception {
         mMockingSession.finishMocking();
+        DeviceConfigUtils.resetPackageVersionCacheForTest();
     }
 
     private TetheringConfiguration getTetheringConfiguration(int... legacyTetherUpstreamTypes) {
@@ -455,4 +486,52 @@
                 mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
         assertTrue(testEnable.isSelectAllPrefixRangeEnabled());
     }
+
+    @Test
+    public void testChooseUpstreamAutomatically() throws Exception {
+        when(mResources.getBoolean(R.bool.config_tether_upstream_automatic))
+                .thenReturn(true);
+        assertChooseUpstreamAutomaticallyIs(true);
+
+        when(mResources.getBoolean(R.bool.config_tether_upstream_automatic))
+                .thenReturn(false);
+        assertChooseUpstreamAutomaticallyIs(false);
+    }
+
+    // The flag override only works on R-
+    @Test @IgnoreAfter(Build.VERSION_CODES.R)
+    public void testChooseUpstreamAutomatically_FlagOverride() throws Exception {
+        when(mResources.getBoolean(R.bool.config_tether_upstream_automatic))
+                .thenReturn(false);
+        setTetherForceUpstreamAutomaticFlagVersion(TEST_PACKAGE_VERSION - 1);
+        assertTrue(DeviceConfigUtils.isFeatureEnabled(mMockContext, NAMESPACE_CONNECTIVITY,
+                TetheringConfiguration.TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION));
+
+        assertChooseUpstreamAutomaticallyIs(true);
+
+        setTetherForceUpstreamAutomaticFlagVersion(0L);
+        assertChooseUpstreamAutomaticallyIs(false);
+
+        setTetherForceUpstreamAutomaticFlagVersion(Long.MAX_VALUE);
+        assertChooseUpstreamAutomaticallyIs(false);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testChooseUpstreamAutomatically_FlagOverrideAfterR() throws Exception {
+        when(mResources.getBoolean(R.bool.config_tether_upstream_automatic))
+                .thenReturn(false);
+        setTetherForceUpstreamAutomaticFlagVersion(TEST_PACKAGE_VERSION - 1);
+        assertChooseUpstreamAutomaticallyIs(false);
+    }
+
+    private void setTetherForceUpstreamAutomaticFlagVersion(Long version) {
+        doReturn(version == null ? null : Long.toString(version)).when(
+                () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY),
+                        eq(TetheringConfiguration.TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION)));
+    }
+
+    private void assertChooseUpstreamAutomaticallyIs(boolean value) {
+        assertEquals(value, new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID)
+                .chooseUpstreamAutomatically);
+    }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index 60fddb5..0611086 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -25,6 +25,9 @@
 import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
 import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
 import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
+import static android.net.ConnectivityManager.TYPE_NONE;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
@@ -64,6 +67,7 @@
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.argThat;
@@ -72,7 +76,9 @@
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -94,10 +100,11 @@
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.hardware.usb.UsbManager;
-import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
 import android.net.EthernetManager;
 import android.net.EthernetManager.TetheredInterfaceCallback;
 import android.net.EthernetManager.TetheredInterfaceRequest;
+import android.net.IConnectivityManager;
 import android.net.IIntResultListener;
 import android.net.INetd;
 import android.net.ITetheringEventCallback;
@@ -159,6 +166,7 @@
 import com.android.internal.util.StateMachine;
 import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent;
 import com.android.testutils.MiscAsserts;
 
 import org.junit.After;
@@ -168,6 +176,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -200,6 +209,10 @@
     private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
     private static final String PROVISIONING_NO_UI_APP_NAME = "no_ui_app";
 
+    private static final int CELLULAR_NETID = 100;
+    private static final int WIFI_NETID = 101;
+    private static final int DUN_NETID = 102;
+
     private static final int DHCPSERVER_START_TIMEOUT_MS = 1000;
 
     @Mock private ApplicationInfo mApplicationInfo;
@@ -212,7 +225,6 @@
     @Mock private UsbManager mUsbManager;
     @Mock private WifiManager mWifiManager;
     @Mock private CarrierConfigManager mCarrierConfigManager;
-    @Mock private UpstreamNetworkMonitor mUpstreamNetworkMonitor;
     @Mock private IPv6TetheringCoordinator mIPv6TetheringCoordinator;
     @Mock private DadProxy mDadProxy;
     @Mock private RouterAdvertisementDaemon mRouterAdvertisementDaemon;
@@ -220,8 +232,6 @@
     @Mock private IDhcpServer mDhcpServer;
     @Mock private INetd mNetd;
     @Mock private UserManager mUserManager;
-    @Mock private NetworkRequest mNetworkRequest;
-    @Mock private ConnectivityManager mCm;
     @Mock private EthernetManager mEm;
     @Mock private TetheringNotificationUpdater mNotificationUpdater;
     @Mock private BpfCoordinator mBpfCoordinator;
@@ -249,6 +259,11 @@
     private OffloadController mOffloadCtrl;
     private PrivateAddressCoordinator mPrivateAddressCoordinator;
     private SoftApCallback mSoftApCallback;
+    private UpstreamNetworkMonitor mUpstreamNetworkMonitor;
+
+    private TestConnectivityManager mCm;
+    private NetworkRequest mNetworkRequest;
+    private NetworkCallback mDefaultNetworkCallback;
 
     private class TestContext extends BroadcastInterceptingContext {
         TestContext(Context base) {
@@ -358,6 +373,11 @@
         }
 
         @Override
+        protected boolean isFeatureEnabled(Context ctx, String featureVersionFlag) {
+            return false;
+        }
+
+        @Override
         protected Resources getResourcesForSubIdWrapper(Context ctx, int subId) {
             return mResources;
         }
@@ -395,7 +415,10 @@
         @Override
         public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx,
                 StateMachine target, SharedLog log, int what) {
+            // Use a real object instead of a mock so that some tests can use a real UNM and some
+            // can use a mock.
             mUpstreamNetworkMonitorSM = target;
+            mUpstreamNetworkMonitor = spy(super.getUpstreamNetworkMonitor(ctx, target, log, what));
             return mUpstreamNetworkMonitor;
         }
 
@@ -475,15 +498,16 @@
         }
     }
 
-    private static UpstreamNetworkState buildMobileUpstreamState(boolean withIPv4,
-            boolean withIPv6, boolean with464xlat) {
+    private static LinkProperties buildUpstreamLinkProperties(String interfaceName,
+            boolean withIPv4, boolean withIPv6, boolean with464xlat) {
         final LinkProperties prop = new LinkProperties();
-        prop.setInterfaceName(TEST_MOBILE_IFNAME);
+        prop.setInterfaceName(interfaceName);
 
         if (withIPv4) {
+            prop.addLinkAddress(new LinkAddress("10.1.2.3/15"));
             prop.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0),
                     InetAddresses.parseNumericAddress("10.0.0.1"),
-                    TEST_MOBILE_IFNAME, RTN_UNICAST));
+                    interfaceName, RTN_UNICAST));
         }
 
         if (withIPv6) {
@@ -493,23 +517,40 @@
                             NetworkConstants.RFC7421_PREFIX_LENGTH));
             prop.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0),
                     InetAddresses.parseNumericAddress("2001:db8::1"),
-                    TEST_MOBILE_IFNAME, RTN_UNICAST));
+                    interfaceName, RTN_UNICAST));
         }
 
         if (with464xlat) {
+            final String clatInterface = "v4-" + interfaceName;
             final LinkProperties stackedLink = new LinkProperties();
-            stackedLink.setInterfaceName(TEST_XLAT_MOBILE_IFNAME);
+            stackedLink.setInterfaceName(clatInterface);
             stackedLink.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0),
                     InetAddresses.parseNumericAddress("192.0.0.1"),
-                    TEST_XLAT_MOBILE_IFNAME, RTN_UNICAST));
+                    clatInterface, RTN_UNICAST));
 
             prop.addStackedLink(stackedLink);
         }
 
+        return prop;
+    }
 
-        final NetworkCapabilities capabilities = new NetworkCapabilities()
-                .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
-        return new UpstreamNetworkState(prop, capabilities, new Network(100));
+    private static NetworkCapabilities buildUpstreamCapabilities(int transport, int... otherCaps) {
+        // TODO: add NOT_VCN_MANAGED.
+        final NetworkCapabilities nc = new NetworkCapabilities()
+                .addTransportType(transport)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+        for (int cap : otherCaps) {
+            nc.addCapability(cap);
+        }
+        return nc;
+    }
+
+    private static UpstreamNetworkState buildMobileUpstreamState(boolean withIPv4,
+            boolean withIPv6, boolean with464xlat) {
+        return new UpstreamNetworkState(
+                buildUpstreamLinkProperties(TEST_MOBILE_IFNAME, withIPv4, withIPv6, with464xlat),
+                buildUpstreamCapabilities(TRANSPORT_CELLULAR),
+                new Network(CELLULAR_NETID));
     }
 
     private static UpstreamNetworkState buildMobileIPv4UpstreamState() {
@@ -528,6 +569,22 @@
         return buildMobileUpstreamState(false, true, true);
     }
 
+    private static UpstreamNetworkState buildWifiUpstreamState() {
+        return new UpstreamNetworkState(
+                buildUpstreamLinkProperties(TEST_WIFI_IFNAME, true /* IPv4 */, true /* IPv6 */,
+                        false /* 464xlat */),
+                buildUpstreamCapabilities(TRANSPORT_WIFI),
+                new Network(WIFI_NETID));
+    }
+
+    private static UpstreamNetworkState buildDunUpstreamState() {
+        return new UpstreamNetworkState(
+                buildUpstreamLinkProperties(TEST_MOBILE_IFNAME, true /* IPv4 */, true /* IPv6 */,
+                        false /* 464xlat */),
+                buildUpstreamCapabilities(TRANSPORT_CELLULAR, NET_CAPABILITY_DUN),
+                new Network(DUN_NETID));
+    }
+
     // See FakeSettingsProvider#clearSettingsProvider() that this needs to be called before and
     // after use.
     @BeforeClass
@@ -573,9 +630,22 @@
         };
         mServiceContext.registerReceiver(mBroadcastReceiver,
                 new IntentFilter(ACTION_TETHER_STATE_CHANGED));
+
+        // TODO: add NOT_VCN_MANAGED here, but more importantly in the production code.
+        // TODO: even better, change TetheringDependencies.getDefaultNetworkRequest() to use
+        // registerSystemDefaultNetworkCallback() on S and above.
+        NetworkCapabilities defaultCaps = new NetworkCapabilities()
+                .addCapability(NET_CAPABILITY_INTERNET);
+        mNetworkRequest = new NetworkRequest(defaultCaps, TYPE_NONE, 1 /* requestId */,
+                NetworkRequest.Type.REQUEST);
+        mCm = spy(new TestConnectivityManager(mServiceContext, mock(IConnectivityManager.class),
+                mNetworkRequest));
+
         mTethering = makeTethering();
         verify(mStatsManager, times(1)).registerNetworkStatsProvider(anyString(), any());
         verify(mNetd).registerUnsolicitedEventListener(any());
+        verifyDefaultNetworkRequestFiled();
+
         final ArgumentCaptor<PhoneStateListener> phoneListenerCaptor =
                 ArgumentCaptor.forClass(PhoneStateListener.class);
         verify(mTelephonyManager).listen(phoneListenerCaptor.capture(),
@@ -612,8 +682,8 @@
     }
 
     private void initTetheringUpstream(UpstreamNetworkState upstreamState) {
-        when(mUpstreamNetworkMonitor.getCurrentPreferredUpstream()).thenReturn(upstreamState);
-        when(mUpstreamNetworkMonitor.selectPreferredUpstreamType(any())).thenReturn(upstreamState);
+        doReturn(upstreamState).when(mUpstreamNetworkMonitor).getCurrentPreferredUpstream();
+        doReturn(upstreamState).when(mUpstreamNetworkMonitor).selectPreferredUpstreamType(any());
     }
 
     private Tethering makeTethering() {
@@ -700,6 +770,19 @@
         mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
     }
 
+    private void verifyDefaultNetworkRequestFiled() {
+        ArgumentCaptor<NetworkCallback> captor = ArgumentCaptor.forClass(NetworkCallback.class);
+        verify(mCm, times(1)).requestNetwork(eq(mNetworkRequest),
+                captor.capture(), any(Handler.class));
+        mDefaultNetworkCallback = captor.getValue();
+        assertNotNull(mDefaultNetworkCallback);
+
+        // The default network request is only ever filed once.
+        verifyNoMoreInteractions(mCm);
+        mUpstreamNetworkMonitor.startTrackDefaultNetwork(mNetworkRequest, mEntitleMgr);
+        verifyNoMoreInteractions(mCm);
+    }
+
     private void verifyInterfaceServingModeStarted(String ifname) throws Exception {
         verify(mNetd, times(1)).interfaceSetCfg(any(InterfaceConfigurationParcel.class));
         verify(mNetd, times(1)).tetherInterfaceAdd(ifname);
@@ -751,9 +834,7 @@
         mTethering.interfaceStatusChanged(TEST_NCM_IFNAME, true);
     }
 
-    private void prepareUsbTethering(UpstreamNetworkState upstreamState) {
-        initTetheringUpstream(upstreamState);
-
+    private void prepareUsbTethering() {
         // Emulate pressing the USB tethering button in Settings UI.
         final TetheringRequestParcel request = createTetheringRequestParcel(TETHERING_USB);
         mTethering.startTethering(request, null);
@@ -768,7 +849,8 @@
     @Test
     public void testUsbConfiguredBroadcastStartsTethering() throws Exception {
         UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
-        prepareUsbTethering(upstreamState);
+        initTetheringUpstream(upstreamState);
+        prepareUsbTethering();
 
         // This should produce no activity of any kind.
         verifyNoMoreInteractions(mNetd);
@@ -863,7 +945,8 @@
     }
 
     private void runUsbTethering(UpstreamNetworkState upstreamState) {
-        prepareUsbTethering(upstreamState);
+        initTetheringUpstream(upstreamState);
+        prepareUsbTethering();
         sendUsbBroadcast(true, true, true, TETHERING_USB);
         mLooper.dispatchAll();
     }
@@ -1004,6 +1087,73 @@
         verify(mUpstreamNetworkMonitor, times(1)).setCurrentUpstream(upstreamState.network);
     }
 
+    @Test
+    public void testAutomaticUpstreamSelection() throws Exception {
+        // Enable automatic upstream selection.
+        when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)).thenReturn(true);
+        sendConfigurationChanged();
+        mLooper.dispatchAll();
+
+        InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
+
+        // Start USB tethering with no current upstream.
+        prepareUsbTethering();
+        sendUsbBroadcast(true, true, true, TETHERING_USB);
+        mLooper.dispatchAll();
+        inOrder.verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+        inOrder.verify(mUpstreamNetworkMonitor).registerMobileNetworkRequest();
+
+        // Pretend cellular connected and expect the upstream to be set.
+        TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
+        mobile.fakeConnect();
+        mCm.makeDefaultNetwork(mobile);
+        mLooper.dispatchAll();
+        inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId);
+
+        // Switch upstreams a few times.
+        // TODO: there may be a race where if the effects of the CONNECTIVITY_ACTION happen before
+        // UpstreamNetworkMonitor gets onCapabilitiesChanged on CALLBACK_DEFAULT_INTERNET, the
+        // upstream does not change. Extend TestConnectivityManager to simulate this condition and
+        // write a test for this.
+        TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
+        wifi.fakeConnect();
+        mCm.makeDefaultNetwork(wifi);
+        mLooper.dispatchAll();
+        inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(wifi.networkId);
+
+        mCm.makeDefaultNetwork(mobile);
+        mLooper.dispatchAll();
+        inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId);
+
+        // Wifi disconnecting should not have any affect since it's not the current upstream.
+        wifi.fakeDisconnect();
+        mLooper.dispatchAll();
+        inOrder.verify(mUpstreamNetworkMonitor, never()).setCurrentUpstream(any());
+
+        // Lose and regain upstream.
+        assertTrue(mUpstreamNetworkMonitor.getCurrentPreferredUpstream().linkProperties
+                .hasIPv4Address());
+        mobile.fakeDisconnect();
+        mCm.makeDefaultNetwork(null);
+        mLooper.dispatchAll();
+        inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(null);
+
+        mobile = new TestNetworkAgent(mCm, buildMobile464xlatUpstreamState());
+        mobile.fakeConnect();
+        mCm.makeDefaultNetwork(mobile);
+        mLooper.dispatchAll();
+        inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId);
+
+        // Check the IP addresses to ensure that the upstream is indeed not the same as the previous
+        // mobile upstream, even though the netId is (unrealistically) the same.
+        assertFalse(mUpstreamNetworkMonitor.getCurrentPreferredUpstream().linkProperties
+                .hasIPv4Address());
+        mobile.fakeDisconnect();
+        mCm.makeDefaultNetwork(null);
+        mLooper.dispatchAll();
+        inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(null);
+    }
+
     private void runNcmTethering() {
         prepareNcmTethering();
         sendUsbBroadcast(true, true, true, TETHERING_NCM);
@@ -1718,12 +1868,12 @@
     }
 
     private void setDataSaverEnabled(boolean enabled) {
-        final Intent intent = new Intent(ACTION_RESTRICT_BACKGROUND_CHANGED);
-        mServiceContext.sendBroadcastAsUser(intent, UserHandle.ALL);
-
         final int status = enabled ? RESTRICT_BACKGROUND_STATUS_ENABLED
                 : RESTRICT_BACKGROUND_STATUS_DISABLED;
-        when(mCm.getRestrictBackgroundStatus()).thenReturn(status);
+        doReturn(status).when(mCm).getRestrictBackgroundStatus();
+
+        final Intent intent = new Intent(ACTION_RESTRICT_BACKGROUND_CHANGED);
+        mServiceContext.sendBroadcastAsUser(intent, UserHandle.ALL);
         mLooper.dispatchAll();
     }
 
@@ -1877,7 +2027,8 @@
         // Verify that onUpstreamCapabilitiesChanged won't be called if not current upstream network
         // capabilities changed.
         final UpstreamNetworkState upstreamState2 = new UpstreamNetworkState(
-                upstreamState.linkProperties, upstreamState.networkCapabilities, new Network(101));
+                upstreamState.linkProperties, upstreamState.networkCapabilities,
+                new Network(WIFI_NETID));
         stateMachine.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState2);
         verify(mNotificationUpdater, never()).onUpstreamCapabilitiesChanged(any());
     }
@@ -1987,7 +2138,7 @@
     public void testHandleIpConflict() throws Exception {
         final Network wifiNetwork = new Network(200);
         final Network[] allNetworks = { wifiNetwork };
-        when(mCm.getAllNetworks()).thenReturn(allNetworks);
+        doReturn(allNetworks).when(mCm).getAllNetworks();
         runUsbTethering(null);
         final ArgumentCaptor<InterfaceConfigurationParcel> ifaceConfigCaptor =
                 ArgumentCaptor.forClass(InterfaceConfigurationParcel.class);
@@ -2014,7 +2165,7 @@
         final Network btNetwork = new Network(201);
         final Network mobileNetwork = new Network(202);
         final Network[] allNetworks = { wifiNetwork, btNetwork, mobileNetwork };
-        when(mCm.getAllNetworks()).thenReturn(allNetworks);
+        doReturn(allNetworks).when(mCm).getAllNetworks();
         runUsbTethering(null);
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
index 232588c..7d735fc 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
@@ -29,7 +29,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
@@ -48,7 +47,6 @@
 import android.net.IpPrefix;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
-import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.util.SharedLog;
@@ -60,6 +58,7 @@
 
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
+import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent;
 
 import org.junit.After;
 import org.junit.Before;
@@ -71,10 +70,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
 
 @RunWith(AndroidJUnit4.class)
@@ -89,6 +85,13 @@
     // any specific TRANSPORT_* is sufficient to identify this request.
     private static final NetworkRequest sDefaultRequest = new NetworkRequest.Builder().build();
 
+    private static final NetworkCapabilities CELL_CAPABILITIES = new NetworkCapabilities.Builder()
+            .addTransportType(TRANSPORT_CELLULAR).addCapability(NET_CAPABILITY_INTERNET).build();
+    private static final NetworkCapabilities DUN_CAPABILITIES = new NetworkCapabilities.Builder()
+            .addTransportType(TRANSPORT_CELLULAR).addCapability(NET_CAPABILITY_DUN).build();
+    private static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities.Builder()
+            .addTransportType(TRANSPORT_WIFI).addCapability(NET_CAPABILITY_INTERNET).build();
+
     @Mock private Context mContext;
     @Mock private EntitlementManager mEntitleMgr;
     @Mock private IConnectivityManager mCS;
@@ -106,7 +109,7 @@
         when(mLog.forSubComponent(anyString())).thenReturn(mLog);
         when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true);
 
-        mCM = spy(new TestConnectivityManager(mContext, mCS));
+        mCM = spy(new TestConnectivityManager(mContext, mCS, sDefaultRequest));
         mSM = new TestStateMachine();
         mUNM = new UpstreamNetworkMonitor(
                 (ConnectivityManager) mCM, mSM, mLog, EVENT_UNM_UPDATE);
@@ -292,7 +295,7 @@
         // There are no networks, so there is nothing to select.
         assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
 
-        final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, TRANSPORT_WIFI);
+        final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES);
         wifiAgent.fakeConnect();
         // WiFi is up, we should prefer it.
         assertSatisfiesLegacyType(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes));
@@ -300,7 +303,7 @@
         // There are no networks, so there is nothing to select.
         assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
 
-        final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR);
+        final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES);
         cellAgent.fakeConnect();
         assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
 
@@ -341,8 +344,7 @@
         mUNM.updateMobileRequiresDun(true);
         assertSatisfiesLegacyType(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes));
 
-        final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR);
-        dunAgent.networkCapabilities.addCapability(NET_CAPABILITY_DUN);
+        final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, DUN_CAPABILITIES);
         dunAgent.fakeConnect();
 
         // WiFi is still preferred.
@@ -374,7 +376,7 @@
         mUNM.updateMobileRequiresDun(false);
 
         // [0] Mobile connects, DUN not required -> mobile selected.
-        final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR);
+        final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES);
         cellAgent.fakeConnect();
         mCM.makeDefaultNetwork(cellAgent);
         assertEquals(cellAgent.networkId, mUNM.getCurrentPreferredUpstream().network);
@@ -385,7 +387,7 @@
         when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true);
 
         // [2] WiFi connects but not validated/promoted to default -> mobile selected.
-        final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, TRANSPORT_WIFI);
+        final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES);
         wifiAgent.fakeConnect();
         assertEquals(cellAgent.networkId, mUNM.getCurrentPreferredUpstream().network);
 
@@ -405,7 +407,7 @@
         // into UNM we should test for this here.
 
         // [6] DUN network arrives -> DUN selected
-        final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR);
+        final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES);
         dunAgent.networkCapabilities.addCapability(NET_CAPABILITY_DUN);
         dunAgent.networkCapabilities.removeCapability(NET_CAPABILITY_INTERNET);
         dunAgent.fakeConnect();
@@ -428,7 +430,7 @@
         final Set<String> alreadySeen = new HashSet<>();
 
         // [1] Pretend Wi-Fi connects.
-        final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, TRANSPORT_WIFI);
+        final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES);
         final LinkProperties wifiLp = wifiAgent.linkProperties;
         wifiLp.setInterfaceName("wlan0");
         final String[] wifi_addrs = {
@@ -455,7 +457,7 @@
         assertEquals(alreadySeen.size(), local.size());
 
         // [2] Pretend mobile connects.
-        final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR);
+        final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES);
         final LinkProperties cellLp = cellAgent.linkProperties;
         cellLp.setInterfaceName("rmnet_data0");
         final String[] cell_addrs = {
@@ -476,9 +478,7 @@
         assertEquals(alreadySeen.size(), local.size());
 
         // [3] Pretend DUN connects.
-        final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR);
-        dunAgent.networkCapabilities.addCapability(NET_CAPABILITY_DUN);
-        dunAgent.networkCapabilities.removeCapability(NET_CAPABILITY_INTERNET);
+        final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, DUN_CAPABILITIES);
         final LinkProperties dunLp = dunAgent.linkProperties;
         dunLp.setInterfaceName("rmnet_data1");
         final String[] dun_addrs = {
@@ -528,11 +528,11 @@
         mUNM.startTrackDefaultNetwork(sDefaultRequest, mEntitleMgr);
         mUNM.startObserveAllNetworks();
         // Setup wifi and make wifi as default network.
-        final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, TRANSPORT_WIFI);
+        final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES);
         wifiAgent.fakeConnect();
         mCM.makeDefaultNetwork(wifiAgent);
         // Setup mobile network.
-        final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR);
+        final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES);
         cellAgent.fakeConnect();
 
         assertSatisfiesLegacyType(TYPE_MOBILE_HIPRI,
@@ -567,187 +567,6 @@
         return false;
     }
 
-    public static class TestConnectivityManager extends ConnectivityManager {
-        public Map<NetworkCallback, Handler> allCallbacks = new HashMap<>();
-        public Set<NetworkCallback> trackingDefault = new HashSet<>();
-        public TestNetworkAgent defaultNetwork = null;
-        public Map<NetworkCallback, NetworkRequest> listening = new HashMap<>();
-        public Map<NetworkCallback, NetworkRequest> requested = new HashMap<>();
-        public Map<NetworkCallback, Integer> legacyTypeMap = new HashMap<>();
-
-        private int mNetworkId = 100;
-
-        public TestConnectivityManager(Context ctx, IConnectivityManager svc) {
-            super(ctx, svc);
-        }
-
-        boolean hasNoCallbacks() {
-            return allCallbacks.isEmpty()
-                    && trackingDefault.isEmpty()
-                    && listening.isEmpty()
-                    && requested.isEmpty()
-                    && legacyTypeMap.isEmpty();
-        }
-
-        boolean onlyHasDefaultCallbacks() {
-            return (allCallbacks.size() == 1)
-                    && (trackingDefault.size() == 1)
-                    && listening.isEmpty()
-                    && requested.isEmpty()
-                    && legacyTypeMap.isEmpty();
-        }
-
-        boolean isListeningForAll() {
-            final NetworkCapabilities empty = new NetworkCapabilities();
-            empty.clearAll();
-
-            for (NetworkRequest req : listening.values()) {
-                if (req.networkCapabilities.equalRequestableCapabilities(empty)) {
-                    return true;
-                }
-            }
-            return false;
-        }
-
-        int getNetworkId() {
-            return ++mNetworkId;
-        }
-
-        void makeDefaultNetwork(TestNetworkAgent agent) {
-            if (Objects.equals(defaultNetwork, agent)) return;
-
-            final TestNetworkAgent formerDefault = defaultNetwork;
-            defaultNetwork = agent;
-
-            for (NetworkCallback cb : trackingDefault) {
-                if (defaultNetwork != null) {
-                    cb.onAvailable(defaultNetwork.networkId);
-                    cb.onCapabilitiesChanged(
-                            defaultNetwork.networkId, defaultNetwork.networkCapabilities);
-                    cb.onLinkPropertiesChanged(
-                            defaultNetwork.networkId, defaultNetwork.linkProperties);
-                }
-            }
-        }
-
-        @Override
-        public void requestNetwork(NetworkRequest req, NetworkCallback cb, Handler h) {
-            assertFalse(allCallbacks.containsKey(cb));
-            allCallbacks.put(cb, h);
-            if (sDefaultRequest.equals(req)) {
-                assertFalse(trackingDefault.contains(cb));
-                trackingDefault.add(cb);
-            } else {
-                assertFalse(requested.containsKey(cb));
-                requested.put(cb, req);
-            }
-        }
-
-        @Override
-        public void requestNetwork(NetworkRequest req, NetworkCallback cb) {
-            fail("Should never be called.");
-        }
-
-        @Override
-        public void requestNetwork(NetworkRequest req,
-                int timeoutMs, int legacyType, Handler h, NetworkCallback cb) {
-            assertFalse(allCallbacks.containsKey(cb));
-            allCallbacks.put(cb, h);
-            assertFalse(requested.containsKey(cb));
-            requested.put(cb, req);
-            assertFalse(legacyTypeMap.containsKey(cb));
-            if (legacyType != ConnectivityManager.TYPE_NONE) {
-                legacyTypeMap.put(cb, legacyType);
-            }
-        }
-
-        @Override
-        public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb, Handler h) {
-            assertFalse(allCallbacks.containsKey(cb));
-            allCallbacks.put(cb, h);
-            assertFalse(listening.containsKey(cb));
-            listening.put(cb, req);
-        }
-
-        @Override
-        public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb) {
-            fail("Should never be called.");
-        }
-
-        @Override
-        public void registerDefaultNetworkCallback(NetworkCallback cb, Handler h) {
-            fail("Should never be called.");
-        }
-
-        @Override
-        public void registerDefaultNetworkCallback(NetworkCallback cb) {
-            fail("Should never be called.");
-        }
-
-        @Override
-        public void unregisterNetworkCallback(NetworkCallback cb) {
-            if (trackingDefault.contains(cb)) {
-                trackingDefault.remove(cb);
-            } else if (listening.containsKey(cb)) {
-                listening.remove(cb);
-            } else if (requested.containsKey(cb)) {
-                requested.remove(cb);
-                legacyTypeMap.remove(cb);
-            } else {
-                fail("Unexpected callback removed");
-            }
-            allCallbacks.remove(cb);
-
-            assertFalse(allCallbacks.containsKey(cb));
-            assertFalse(trackingDefault.contains(cb));
-            assertFalse(listening.containsKey(cb));
-            assertFalse(requested.containsKey(cb));
-        }
-    }
-
-    public static class TestNetworkAgent {
-        public final TestConnectivityManager cm;
-        public final Network networkId;
-        public final int transportType;
-        public final NetworkCapabilities networkCapabilities;
-        public final LinkProperties linkProperties;
-
-        public TestNetworkAgent(TestConnectivityManager cm, int transportType) {
-            this.cm = cm;
-            this.networkId = new Network(cm.getNetworkId());
-            this.transportType = transportType;
-            networkCapabilities = new NetworkCapabilities();
-            networkCapabilities.addTransportType(transportType);
-            networkCapabilities.addCapability(NET_CAPABILITY_INTERNET);
-            linkProperties = new LinkProperties();
-        }
-
-        public void fakeConnect() {
-            for (NetworkCallback cb : cm.listening.keySet()) {
-                cb.onAvailable(networkId);
-                cb.onCapabilitiesChanged(networkId, copy(networkCapabilities));
-                cb.onLinkPropertiesChanged(networkId, copy(linkProperties));
-            }
-        }
-
-        public void fakeDisconnect() {
-            for (NetworkCallback cb : cm.listening.keySet()) {
-                cb.onLost(networkId);
-            }
-        }
-
-        public void sendLinkProperties() {
-            for (NetworkCallback cb : cm.listening.keySet()) {
-                cb.onLinkPropertiesChanged(networkId, copy(linkProperties));
-            }
-        }
-
-        @Override
-        public String toString() {
-            return String.format("TestNetworkAgent: %s %s", networkId, networkCapabilities);
-        }
-    }
-
     public static class TestStateMachine extends StateMachine {
         public final ArrayList<Message> messages = new ArrayList<>();
         private final State mLoggingState = new LoggingState();
@@ -775,14 +594,6 @@
         }
     }
 
-    static NetworkCapabilities copy(NetworkCapabilities nc) {
-        return new NetworkCapabilities(nc);
-    }
-
-    static LinkProperties copy(LinkProperties lp) {
-        return new LinkProperties(lp);
-    }
-
     static void assertPrefixSet(Set<IpPrefix> prefixes, boolean expectation, String... expected) {
         final Set<String> expectedSet = new HashSet<>();
         Collections.addAll(expectedSet, expected);
@@ -797,4 +608,4 @@
                     expectation, prefixes.contains(new IpPrefix(expectedPrefix)));
         }
     }
-}
+}
\ No newline at end of file
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
index 813e6c7..f351b47 100644
--- a/tests/cts/hostside/app/Android.bp
+++ b/tests/cts/hostside/app/Android.bp
@@ -30,6 +30,7 @@
         "ctstestrunner-axt",
         "ub-uiautomator",
         "CtsHostsideNetworkTestsAidl",
+        "modules-utils-build",
     ],
     libs: [
         "android.test.runner",
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index a663cd6..9b437e6 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -75,6 +75,7 @@
 import android.util.Log;
 
 import com.android.compatibility.common.util.BlockingBroadcastReceiver;
+import com.android.modules.utils.build.SdkLevel;
 
 import java.io.Closeable;
 import java.io.FileDescriptor;
@@ -740,13 +741,14 @@
                 getInstrumentation().getTargetContext(), MyVpnService.ACTION_ESTABLISHED);
         receiver.register();
 
-
         // Expect the system default network not to change.
         final NeverChangeNetworkCallback neverChangeCallback = new NeverChangeNetworkCallback();
         final Network defaultNetwork = mCM.getActiveNetwork();
-        runWithShellPermissionIdentity(() ->
-                mCM.registerSystemDefaultNetworkCallback(neverChangeCallback,
-                        new Handler(Looper.getMainLooper())), NETWORK_SETTINGS);
+        if (SdkLevel.isAtLeastS()) {
+            runWithShellPermissionIdentity(() ->
+                    mCM.registerSystemDefaultNetworkCallback(neverChangeCallback,
+                            new Handler(Looper.getMainLooper())), NETWORK_SETTINGS);
+        }
 
         FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
 
@@ -765,19 +767,21 @@
 
         checkTrafficOnVpn();
 
-        expectVpnTransportInfo(mCM.getActiveNetwork());
+        maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
 
-        // Check that system default network callback has not seen any network changes, even though
-        // the app's default network changed. This needs to be done before testing private
-        // DNS because checkStrictModePrivateDns will set the private DNS server to a nonexistent
-        // name, which will cause validation to fail and cause the default network to switch (e.g.,
-        // from wifi to cellular).
-        assertEquals(defaultNetwork, neverChangeCallback.getFirstNetwork());
         assertNotEqual(defaultNetwork, mCM.getActiveNetwork());
-        neverChangeCallback.assertNeverChanged();
-        runWithShellPermissionIdentity(
-                () ->  mCM.unregisterNetworkCallback(neverChangeCallback),
-                NETWORK_SETTINGS);
+        if (SdkLevel.isAtLeastS()) {
+            // Check that system default network callback has not seen any network changes, even
+            // though the app's default network changed. This needs to be done before testing
+            // private DNS because checkStrictModePrivateDns will set the private DNS server to
+            // a nonexistent name, which will cause validation to fail and cause the default
+            // network to switch (e.g., from wifi to cellular).
+            assertEquals(defaultNetwork, neverChangeCallback.getFirstNetwork());
+            neverChangeCallback.assertNeverChanged();
+            runWithShellPermissionIdentity(
+                    () -> mCM.unregisterNetworkCallback(neverChangeCallback),
+                    NETWORK_SETTINGS);
+        }
 
         checkStrictModePrivateDns();
 
@@ -799,7 +803,7 @@
 
         checkTrafficOnVpn();
 
-        expectVpnTransportInfo(mCM.getActiveNetwork());
+        maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
 
         checkStrictModePrivateDns();
     }
@@ -988,7 +992,7 @@
         assertTrue(isNetworkMetered(mNetwork));
         assertTrue(mCM.isActiveNetworkMetered());
 
-        expectVpnTransportInfo(mCM.getActiveNetwork());
+        maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
     }
 
     public void testVpnMeterednessWithNullUnderlyingNetwork() throws Exception {
@@ -1016,7 +1020,7 @@
         // Meteredness based on VPN capabilities and CM#isActiveNetworkMetered should be in sync.
         assertEquals(isNetworkMetered(mNetwork), mCM.isActiveNetworkMetered());
 
-        expectVpnTransportInfo(mCM.getActiveNetwork());
+        maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
     }
 
     public void testVpnMeterednessWithNonNullUnderlyingNetwork() throws Exception {
@@ -1045,7 +1049,7 @@
         // Meteredness based on VPN capabilities and CM#isActiveNetworkMetered should be in sync.
         assertEquals(isNetworkMetered(mNetwork), mCM.isActiveNetworkMetered());
 
-        expectVpnTransportInfo(mCM.getActiveNetwork());
+        maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
     }
 
     public void testAlwaysMeteredVpnWithNullUnderlyingNetwork() throws Exception {
@@ -1071,7 +1075,7 @@
         assertTrue(isNetworkMetered(mNetwork));
         assertTrue(mCM.isActiveNetworkMetered());
 
-        expectVpnTransportInfo(mCM.getActiveNetwork());
+        maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
     }
 
     public void testAlwaysMeteredVpnWithNonNullUnderlyingNetwork() throws Exception {
@@ -1098,7 +1102,7 @@
         assertTrue(isNetworkMetered(mNetwork));
         assertTrue(mCM.isActiveNetworkMetered());
 
-        expectVpnTransportInfo(mCM.getActiveNetwork());
+        maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
     }
 
     public void testB141603906() throws Exception {
@@ -1148,7 +1152,8 @@
         }
     }
 
-    private void expectVpnTransportInfo(Network network) {
+    private void maybeExpectVpnTransportInfo(Network network) {
+        if (!SdkLevel.isAtLeastS()) return;
         final NetworkCapabilities vpnNc = mCM.getNetworkCapabilities(network);
         assertTrue(vpnNc.hasTransport(TRANSPORT_VPN));
         final TransportInfo ti = vpnNc.getTransportInfo();
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
index ce20379..37420bf 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -152,8 +152,10 @@
             // build a meaningful error message
             StringBuilder errorBuilder = new StringBuilder("on-device tests failed:\n");
             for (Map.Entry<TestDescription, TestResult> resultEntry :
-                result.getTestResults().entrySet()) {
-                if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) {
+                    result.getTestResults().entrySet()) {
+                final TestStatus testStatus = resultEntry.getValue().getStatus();
+                if (!TestStatus.PASSED.equals(testStatus)
+                        && !TestStatus.ASSUMPTION_FAILURE.equals(testStatus)) {
                     errorBuilder.append(resultEntry.getKey().toString());
                     errorBuilder.append(":\n");
                     errorBuilder.append(resultEntry.getValue().getStackTrace());
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 0a67676..cd69b13 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -41,13 +41,14 @@
     ],
     jarjar_rules: "jarjar-rules-shared.txt",
     static_libs: [
+        "bouncycastle-unbundled",
         "FrameworksNetCommonTests",
-        "TestNetworkStackLib",
         "core-tests-support",
         "cts-net-utils",
         "ctstestrunner-axt",
         "junit",
         "junit-params",
+        "modules-utils-build",
         "net-utils-framework-common",
         "truth-prebuilt",
     ],
@@ -64,6 +65,10 @@
 android_test {
     name: "CtsNetTestCases",
     defaults: ["CtsNetTestCasesDefaults"],
+    // TODO: CTS should not depend on the entirety of the networkstack code.
+    static_libs: [
+        "NetworkStackApiCurrentLib",
+    ],
     test_suites: [
         "cts",
         "general-tests",
@@ -77,6 +82,10 @@
 android_test {
     name: "CtsNetTestCasesLatestSdk",
     defaults: ["CtsNetTestCasesDefaults"],
+    // TODO: CTS should not depend on the entirety of the networkstack code.
+    static_libs: [
+        "NetworkStackApiStableLib",
+    ],
     jni_uses_sdk_apis: true,
     min_sdk_version: "29",
     target_sdk_version: "30",
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index ce874d1..43e9970 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -85,7 +85,6 @@
 import android.net.NetworkRequest;
 import android.net.NetworkUtils;
 import android.net.SocketKeepalive;
-import android.net.StringNetworkSpecifier;
 import android.net.TestNetworkInterface;
 import android.net.TestNetworkManager;
 import android.net.cts.util.CtsNetUtils;
@@ -110,6 +109,10 @@
 
 import com.android.internal.util.ArrayUtils;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
+import com.android.networkstack.apishim.ConstantsShim;
+import com.android.networkstack.apishim.common.ConnectivityManagerShim;
+import com.android.testutils.CompatUtil;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.RecorderCallback.CallbackEntry;
@@ -195,6 +198,7 @@
     private Context mContext;
     private Instrumentation mInstrumentation;
     private ConnectivityManager mCm;
+    private ConnectivityManagerShim mCmShim;
     private WifiManager mWifiManager;
     private PackageManager mPackageManager;
     private final HashMap<Integer, NetworkConfig> mNetworks =
@@ -207,6 +211,7 @@
         mInstrumentation = InstrumentationRegistry.getInstrumentation();
         mContext = mInstrumentation.getContext();
         mCm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+        mCmShim = ConnectivityManagerShimImpl.newInstance(mContext);
         mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
         mPackageManager = mContext.getPackageManager();
         mCtsNetUtils = new CtsNetUtils(mContext);
@@ -522,9 +527,9 @@
         mCm.registerDefaultNetworkCallback(defaultTrackingCallback);
 
         final TestNetworkCallback systemDefaultTrackingCallback = new TestNetworkCallback();
-        if (SdkLevel.isAtLeastS()) {
+        if (shouldTestSApis()) {
             runWithShellPermissionIdentity(() ->
-                    mCm.registerSystemDefaultNetworkCallback(systemDefaultTrackingCallback,
+                    mCmShim.registerSystemDefaultNetworkCallback(systemDefaultTrackingCallback,
                             new Handler(Looper.getMainLooper())),
                     NETWORK_SETTINGS);
         }
@@ -544,7 +549,7 @@
             assertNotNull("Did not receive onAvailable on default network callback",
                     defaultTrackingCallback.waitForAvailable());
 
-            if (SdkLevel.isAtLeastS()) {
+            if (shouldTestSApis()) {
                 assertNotNull("Did not receive onAvailable on system default network callback",
                         systemDefaultTrackingCallback.waitForAvailable());
             }
@@ -553,7 +558,7 @@
         } finally {
             mCm.unregisterNetworkCallback(callback);
             mCm.unregisterNetworkCallback(defaultTrackingCallback);
-            if (SdkLevel.isAtLeastS()) {
+            if (shouldTestSApis()) {
                 runWithShellPermissionIdentity(
                         () -> mCm.unregisterNetworkCallback(systemDefaultTrackingCallback),
                         NETWORK_SETTINGS);
@@ -1578,11 +1583,12 @@
     public void testRequestBackgroundNetwork() throws Exception {
         // Create a tun interface. Use the returned interface name as the specifier to create
         // a test network request.
-        final TestNetworkInterface testNetworkInterface = runWithShellPermissionIdentity(() -> {
-            final TestNetworkManager tnm =
-                    mContext.getSystemService(TestNetworkManager.class);
-            return tnm.createTunInterface(new LinkAddress[]{TEST_LINKADDR});
-        }, android.Manifest.permission.MANAGE_TEST_NETWORKS,
+        final TestNetworkManager tnm = runWithShellPermissionIdentity(() ->
+                mContext.getSystemService(TestNetworkManager.class),
+                android.Manifest.permission.MANAGE_TEST_NETWORKS);
+        final TestNetworkInterface testNetworkInterface = runWithShellPermissionIdentity(() ->
+                    tnm.createTunInterface(new LinkAddress[]{TEST_LINKADDR}),
+                android.Manifest.permission.MANAGE_TEST_NETWORKS,
                 android.Manifest.permission.NETWORK_SETTINGS);
         assertNotNull(testNetworkInterface);
 
@@ -1591,33 +1597,33 @@
                 // Test networks do not have NOT_VPN or TRUSTED capabilities by default
                 .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
                 .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
-                .setNetworkSpecifier(
-                        new StringNetworkSpecifier(testNetworkInterface.getInterfaceName()))
+                .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
+                        testNetworkInterface.getInterfaceName()))
                 .build();
 
         // Verify background network cannot be requested without NETWORK_SETTINGS permission.
         final TestableNetworkCallback callback = new TestableNetworkCallback();
+        final Handler handler = new Handler(Looper.getMainLooper());
         assertThrows(SecurityException.class,
-                () -> mCm.requestBackgroundNetwork(testRequest, null, callback));
+                () -> mCmShim.requestBackgroundNetwork(testRequest, handler, callback));
 
+        Network testNetwork = null;
         try {
             // Request background test network via Shell identity which has NETWORK_SETTINGS
             // permission granted.
             runWithShellPermissionIdentity(
-                    () -> mCm.requestBackgroundNetwork(testRequest, null, callback),
-                    android.Manifest.permission.NETWORK_SETTINGS);
+                    () -> mCmShim.requestBackgroundNetwork(testRequest, handler, callback),
+                    new String[] { android.Manifest.permission.NETWORK_SETTINGS });
 
             // Register the test network agent which has no foreground request associated to it.
             // And verify it can satisfy the background network request just fired.
             final Binder binder = new Binder();
-            runWithShellPermissionIdentity(() -> {
-                final TestNetworkManager tnm =
-                        mContext.getSystemService(TestNetworkManager.class);
-                tnm.setupTestNetwork(testNetworkInterface.getInterfaceName(), binder);
-            }, android.Manifest.permission.MANAGE_TEST_NETWORKS,
-                    android.Manifest.permission.NETWORK_SETTINGS);
+            runWithShellPermissionIdentity(() ->
+                    tnm.setupTestNetwork(testNetworkInterface.getInterfaceName(), binder),
+                    new String[] { android.Manifest.permission.MANAGE_TEST_NETWORKS,
+                            android.Manifest.permission.NETWORK_SETTINGS });
             waitForAvailable(callback);
-            final Network testNetwork = callback.getLastAvailableNetwork();
+            testNetwork = callback.getLastAvailableNetwork();
             assertNotNull(testNetwork);
 
             // The test network that has just connected is a foreground network,
@@ -1633,7 +1639,25 @@
             assertFalse("expected background network, but got " + nc,
                     nc.hasCapability(NET_CAPABILITY_FOREGROUND));
         } finally {
+            final Network n = testNetwork;
+            runWithShellPermissionIdentity(() -> {
+                if (null != n) {
+                    tnm.teardownTestNetwork(n);
+                    callback.eventuallyExpect(CallbackEntry.LOST,
+                            NETWORK_CALLBACK_TIMEOUT_MS,
+                            lost -> n.equals(lost.getNetwork()));
+                }
+                testNetworkInterface.getFileDescriptor().close();
+            }, new String[] { android.Manifest.permission.MANAGE_TEST_NETWORKS });
             mCm.unregisterNetworkCallback(callback);
         }
     }
+
+    /**
+     * Whether to test S+ APIs. This requires a) that the test be running on an S+ device, and
+     * b) that the code be compiled against shims new enough to access these APIs.
+     */
+    private boolean shouldTestSApis() {
+        return SdkLevel.isAtLeastS() && ConstantsShim.VERSION > Build.VERSION_CODES.R;
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 8f2d93d..c6d8d65 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -55,10 +55,10 @@
 import androidx.test.InstrumentationRegistry;
 
 import com.android.internal.util.HexDump;
-import com.android.internal.org.bouncycastle.x509.X509V1CertificateGenerator;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
 
+import org.bouncycastle.x509.X509V1CertificateGenerator;
 import org.junit.After;
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index aea33ca..f17e50c 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -35,6 +35,7 @@
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN
 import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
 import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
@@ -45,7 +46,6 @@
 import android.net.NetworkRequest
 import android.net.RouteInfo
 import android.net.SocketKeepalive
-import android.net.StringNetworkSpecifier
 import android.net.Uri
 import android.net.VpnManager
 import android.net.VpnTransportInfo
@@ -67,7 +67,9 @@
 import androidx.test.InstrumentationRegistry
 import com.android.connectivity.aidl.INetworkAgent
 import com.android.connectivity.aidl.INetworkAgentRegistry
+import com.android.modules.utils.build.SdkLevel
 import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.CompatUtil
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.RecorderCallback.CallbackEntry.Available
@@ -79,7 +81,6 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.argThat
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mockito.doReturn
@@ -216,8 +217,6 @@
             data class OnSignalStrengthThresholdsUpdated(val thresholds: IntArray) : CallbackEntry()
         }
 
-        fun getName(): String? = (nc.getNetworkSpecifier() as? StringNetworkSpecifier)?.specifier
-
         override fun onBandwidthUpdateRequested() {
             history.add(OnBandwidthUpdateRequested)
         }
@@ -321,8 +320,11 @@
             addCapability(NET_CAPABILITY_NOT_SUSPENDED)
             addCapability(NET_CAPABILITY_NOT_ROAMING)
             addCapability(NET_CAPABILITY_NOT_VPN)
+            if (SdkLevel.isAtLeastS()) {
+                addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+            }
             if (null != name) {
-                setNetworkSpecifier(StringNetworkSpecifier(name))
+                setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(name))
             }
         }
         val lp = initialLp ?: LinkProperties().apply {
@@ -498,12 +500,12 @@
         val request1 = NetworkRequest.Builder()
                 .clearCapabilities()
                 .addTransportType(TRANSPORT_TEST)
-                .setNetworkSpecifier(StringNetworkSpecifier(name1))
+                .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(name1))
                 .build()
         val request2 = NetworkRequest.Builder()
                 .clearCapabilities()
                 .addTransportType(TRANSPORT_TEST)
-                .setNetworkSpecifier(StringNetworkSpecifier(name2))
+                .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(name2))
                 .build()
         val callback1 = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
         val callback2 = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
@@ -558,6 +560,9 @@
             addTransportType(TRANSPORT_VPN)
             removeCapability(NET_CAPABILITY_NOT_VPN)
             setTransportInfo(VpnTransportInfo(VpnManager.TYPE_VPN_SERVICE))
+            if (SdkLevel.isAtLeastS()) {
+                addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+            }
         }
         val defaultNetwork = mCM.activeNetwork
         assertNotNull(defaultNetwork)
@@ -626,7 +631,7 @@
                 argThat<NetworkInfo> { it.detailedState == NetworkInfo.DetailedState.CONNECTING },
                 any(LinkProperties::class.java),
                 any(NetworkCapabilities::class.java),
-                anyInt() /* score */,
+                any() /* score */,
                 any(NetworkAgentConfig::class.java),
                 eq(NetworkProvider.ID_NONE))
     }
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index d118c8a..30c4e72 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -16,8 +16,12 @@
 
 package android.net.cts;
 
+import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_SUPL;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
 import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
@@ -29,6 +33,7 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
+import android.annotation.NonNull;
 import android.net.MacAddress;
 import android.net.MatchAllNetworkSpecifier;
 import android.net.NetworkCapabilities;
@@ -43,6 +48,8 @@
 
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.build.SdkLevel;
+import com.android.networkstack.apishim.ConstantsShim;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 
@@ -152,29 +159,44 @@
                 .getRequestorPackageName());
     }
 
+    private void addNotVcnManagedCapability(@NonNull NetworkCapabilities nc) {
+        if (SdkLevel.isAtLeastS()) {
+            nc.addCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED);
+        }
+    }
+
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.Q)
     public void testCanBeSatisfiedBy() {
         final LocalNetworkSpecifier specifier1 = new LocalNetworkSpecifier(1234 /* id */);
         final LocalNetworkSpecifier specifier2 = new LocalNetworkSpecifier(5678 /* id */);
 
+        // Some requests are adding NOT_VCN_MANAGED capability automatically. Add it to the
+        // capabilities below for bypassing the check.
         final NetworkCapabilities capCellularMmsInternet = new NetworkCapabilities()
                 .addTransportType(TRANSPORT_CELLULAR)
                 .addCapability(NET_CAPABILITY_MMS)
                 .addCapability(NET_CAPABILITY_INTERNET);
+        addNotVcnManagedCapability(capCellularMmsInternet);
         final NetworkCapabilities capCellularVpnMmsInternet =
                 new NetworkCapabilities(capCellularMmsInternet).addTransportType(TRANSPORT_VPN);
+        addNotVcnManagedCapability(capCellularVpnMmsInternet);
         final NetworkCapabilities capCellularMmsInternetSpecifier1 =
                 new NetworkCapabilities(capCellularMmsInternet).setNetworkSpecifier(specifier1);
+        addNotVcnManagedCapability(capCellularMmsInternetSpecifier1);
         final NetworkCapabilities capVpnInternetSpecifier1 = new NetworkCapabilities()
                 .addCapability(NET_CAPABILITY_INTERNET)
                 .addTransportType(TRANSPORT_VPN)
                 .setNetworkSpecifier(specifier1);
+        addNotVcnManagedCapability(capVpnInternetSpecifier1);
         final NetworkCapabilities capCellularMmsInternetMatchallspecifier =
                 new NetworkCapabilities(capCellularMmsInternet)
-                    .setNetworkSpecifier(new MatchAllNetworkSpecifier());
+                        .setNetworkSpecifier(new MatchAllNetworkSpecifier());
+        addNotVcnManagedCapability(capCellularMmsInternetMatchallspecifier);
         final NetworkCapabilities capCellularMmsInternetSpecifier2 =
-                new NetworkCapabilities(capCellularMmsInternet).setNetworkSpecifier(specifier2);
+                new NetworkCapabilities(capCellularMmsInternet)
+                        .setNetworkSpecifier(specifier2);
+        addNotVcnManagedCapability(capCellularMmsInternetSpecifier2);
 
         final NetworkRequest requestCellularInternetSpecifier1 = new NetworkRequest.Builder()
                 .addTransportType(TRANSPORT_CELLULAR)
@@ -239,7 +261,8 @@
 
         final NetworkCapabilities capCellInternetBWSpecifier1Signal =
                 new NetworkCapabilities.Builder(capCellInternetBWSpecifier1)
-                    .setSignalStrength(-123).build();
+                        .setSignalStrength(-123).build();
+        addNotVcnManagedCapability(capCellInternetBWSpecifier1Signal);
         assertCorrectlySatisfies(true, requestCombination,
                 capCellInternetBWSpecifier1Signal);
 
@@ -273,4 +296,80 @@
         assertEquals(Process.INVALID_UID, new NetworkRequest.Builder()
                 .clearCapabilities().build().getRequestorUid());
     }
+
+    // TODO: 1. Refactor test cases with helper method.
+    //       2. Test capability that does not yet exist.
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testBypassingVcnForNonInternetRequest() {
+        // Make an empty request. Verify the NOT_VCN_MANAGED is added.
+        final NetworkRequest emptyRequest = new NetworkRequest.Builder().build();
+        assertTrue(emptyRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+        // Make a request explicitly add NOT_VCN_MANAGED. Verify the NOT_VCN_MANAGED is preserved.
+        final NetworkRequest mmsAddNotVcnRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_MMS)
+                .addCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED)
+                .build();
+        assertTrue(mmsAddNotVcnRequest.hasCapability(
+                ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+        // Similar to above, but the opposite order.
+        final NetworkRequest mmsAddNotVcnRequest2 = new NetworkRequest.Builder()
+                .addCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED)
+                .addCapability(NET_CAPABILITY_MMS)
+                .build();
+        assertTrue(mmsAddNotVcnRequest2.hasCapability(
+                ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+        // Make a request explicitly remove NOT_VCN_MANAGED. Verify the NOT_VCN_MANAGED is removed.
+        final NetworkRequest removeNotVcnRequest = new NetworkRequest.Builder()
+                .removeCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED).build();
+        assertFalse(removeNotVcnRequest.hasCapability(
+                ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+        // Make a request add some capability inside VCN supported capabilities.
+        // Verify the NOT_VCN_MANAGED is added.
+        final NetworkRequest notRoamRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_NOT_ROAMING).build();
+        assertTrue(notRoamRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+        // Make a internet request. Verify the NOT_VCN_MANAGED is added.
+        final NetworkRequest internetRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET).build();
+        assertTrue(internetRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+        // Make a internet request which explicitly removed NOT_VCN_MANAGED.
+        // Verify the NOT_VCN_MANAGED is removed.
+        final NetworkRequest internetRemoveNotVcnRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .removeCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED).build();
+        assertFalse(internetRemoveNotVcnRequest.hasCapability(
+                ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+        // Make a normal MMS request. Verify the request could bypass VCN.
+        final NetworkRequest mmsRequest =
+                new NetworkRequest.Builder().addCapability(NET_CAPABILITY_MMS).build();
+        assertFalse(mmsRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+        // Make a SUPL request along with internet. Verify NOT_VCN_MANAGED is not added since
+        // SUPL is not in the supported list.
+        final NetworkRequest suplWithInternetRequest = new NetworkRequest.Builder()
+                        .addCapability(NET_CAPABILITY_SUPL)
+                        .addCapability(NET_CAPABILITY_INTERNET).build();
+        assertFalse(suplWithInternetRequest.hasCapability(
+                ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+        // Make a FOTA request with explicitly add NOT_VCN_MANAGED capability. Verify
+        // NOT_VCN_MANAGED is preserved.
+        final NetworkRequest fotaRequest = new NetworkRequest.Builder()
+                        .addCapability(NET_CAPABILITY_FOTA)
+                        .addCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED).build();
+        assertTrue(fotaRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+        // Make a DUN request, which is in {@code VCN_SUPPORTED_CAPABILITIES}.
+        // Verify NOT_VCN_MANAGED is preserved.
+        final NetworkRequest dunRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_DUN).build();
+        assertTrue(dunRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+    }
 }