Merge "Increase the number of test service names"
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 6996ad9..780ba26 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -23,6 +23,12 @@
     },
     {
       "name": "TetheringIntegrationTests"
+    },
+    {
+      "name": "traffic_controller_unit_test"
+    },
+    {
+      "name": "libnetworkstats_test"
     }
   ],
   "postsubmit": [
@@ -32,6 +38,16 @@
     // TODO: move to presubmit when known green.
     {
       "name": "bpf_existence_test"
+    },
+    {
+      "name": "libclat_test"
+    },
+    {
+      "name": "traffic_controller_unit_test",
+      "keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"]
+    },
+    {
+      "name": "libnetworkstats_test"
     }
   ],
   "mainline-presubmit": [
@@ -48,6 +64,12 @@
     },
     {
       "name": "ConnectivityCoverageTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
+    },
+    {
+      "name": "traffic_controller_unit_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
+    },
+    {
+      "name": "libnetworkstats_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
     }
   ],
   "mainline-postsubmit": [
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 0e04dbe..ecbaf61 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -53,6 +53,7 @@
         "framework-statsd.stubs.module_lib",
         "framework-tethering.impl",
         "framework-wifi",
+        "framework-bluetooth",
         "unsupportedappusage",
     ],
     plugins: ["java_api_finder"],
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index c72d3a6..53a14ad 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -31,6 +31,7 @@
     // package names and keys, so that apex will be unused anyway.
     apps: ["TetheringNext"], // Replace to "Tethering" if ConnectivityNextEnableDefaults is false.
 }
+enable_tethering_next_apex = true
 // This is a placeholder comment to avoid merge conflicts
 // as the above target may have different "enabled" values
 // depending on the branch
@@ -51,7 +52,8 @@
         first: {
             jni_libs: [
                 "libservice-connectivity",
-                "libcom_android_connectivity_com_android_net_module_util_jni"
+                "libcom_android_connectivity_com_android_net_module_util_jni",
+                "libtraffic_controller_jni",
             ],
         },
         both: {
@@ -63,6 +65,9 @@
     ],
     canned_fs_config: "canned_fs_config",
     bpfs: [
+        "clatd.o_mainline",
+        "netd.o_mainline",
+        "dscp_policy.o",
         "offload.o",
         "test.o",
     ],
@@ -117,8 +122,13 @@
     // Additional hidden API flag files to override the defaults. This must only be
     // modified by the Soong or platform compat team.
     hidden_api: {
-        max_target_r_low_priority: ["hiddenapi/hiddenapi-max-target-r-loprio.txt"],
-        max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
+        max_target_r_low_priority: [
+            "hiddenapi/hiddenapi-max-target-r-loprio.txt",
+	],
+        max_target_o_low_priority: [
+            "hiddenapi/hiddenapi-max-target-o-low-priority.txt",
+            "hiddenapi/hiddenapi-max-target-o-low-priority-tiramisu.txt",
+	],
         unsupported: ["hiddenapi/hiddenapi-unsupported.txt"],
     },
 }
@@ -133,6 +143,7 @@
     name: "com.android.tethering.inprocess",
     base: "com.android.tethering",
     package_name: "com.android.tethering.inprocess",
+    enabled: enable_tethering_next_apex,
     apps: [
         "ServiceConnectivityResources",
         "InProcessTethering",
diff --git a/Tethering/apex/canned_fs_config b/Tethering/apex/canned_fs_config
index 44c57ab..06e9617 100644
--- a/Tethering/apex/canned_fs_config
+++ b/Tethering/apex/canned_fs_config
@@ -1,2 +1,2 @@
 /bin/for-system 0 1000 0550
-/bin/for-system/clatd 1029 1029 06555
+/bin/for-system/clatd 1029 1029 06755
diff --git a/Tethering/apex/hiddenapi/hiddenapi-max-target-o-low-priority-tiramisu.txt b/Tethering/apex/hiddenapi/hiddenapi-max-target-o-low-priority-tiramisu.txt
new file mode 100644
index 0000000..88c77f2
--- /dev/null
+++ b/Tethering/apex/hiddenapi/hiddenapi-max-target-o-low-priority-tiramisu.txt
@@ -0,0 +1,87 @@
+Landroid/net/nsd/DnsSdTxtRecord;-><init>()V
+Landroid/net/nsd/DnsSdTxtRecord;-><init>(Landroid/net/nsd/DnsSdTxtRecord;)V
+Landroid/net/nsd/DnsSdTxtRecord;-><init>([B)V
+Landroid/net/nsd/DnsSdTxtRecord;->contains(Ljava/lang/String;)Z
+Landroid/net/nsd/DnsSdTxtRecord;->CREATOR:Landroid/os/Parcelable$Creator;
+Landroid/net/nsd/DnsSdTxtRecord;->get(Ljava/lang/String;)Ljava/lang/String;
+Landroid/net/nsd/DnsSdTxtRecord;->getKey(I)Ljava/lang/String;
+Landroid/net/nsd/DnsSdTxtRecord;->getRawData()[B
+Landroid/net/nsd/DnsSdTxtRecord;->getValue(I)[B
+Landroid/net/nsd/DnsSdTxtRecord;->getValue(Ljava/lang/String;)[B
+Landroid/net/nsd/DnsSdTxtRecord;->getValueAsString(I)Ljava/lang/String;
+Landroid/net/nsd/DnsSdTxtRecord;->insert([B[BI)V
+Landroid/net/nsd/DnsSdTxtRecord;->keyCount()I
+Landroid/net/nsd/DnsSdTxtRecord;->mData:[B
+Landroid/net/nsd/DnsSdTxtRecord;->mSeperator:B
+Landroid/net/nsd/DnsSdTxtRecord;->remove(Ljava/lang/String;)I
+Landroid/net/nsd/DnsSdTxtRecord;->set(Ljava/lang/String;Ljava/lang/String;)V
+Landroid/net/nsd/DnsSdTxtRecord;->size()I
+Landroid/net/nsd/INsdManager$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
+Landroid/net/nsd/INsdManager$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
+Landroid/net/nsd/INsdManager$Stub$Proxy;->getMessenger()Landroid/os/Messenger;
+Landroid/net/nsd/INsdManager$Stub$Proxy;->mRemote:Landroid/os/IBinder;
+Landroid/net/nsd/INsdManager$Stub$Proxy;->setEnabled(Z)V
+Landroid/net/nsd/INsdManager$Stub;-><init>()V
+Landroid/net/nsd/INsdManager$Stub;->DESCRIPTOR:Ljava/lang/String;
+Landroid/net/nsd/INsdManager$Stub;->TRANSACTION_getMessenger:I
+Landroid/net/nsd/INsdManager$Stub;->TRANSACTION_setEnabled:I
+Landroid/net/nsd/INsdManager;->setEnabled(Z)V
+Landroid/net/nsd/NsdManager;-><init>(Landroid/content/Context;Landroid/net/nsd/INsdManager;)V
+Landroid/net/nsd/NsdManager;->BASE:I
+Landroid/net/nsd/NsdManager;->checkListener(Ljava/lang/Object;)V
+Landroid/net/nsd/NsdManager;->checkProtocol(I)V
+Landroid/net/nsd/NsdManager;->checkServiceInfo(Landroid/net/nsd/NsdServiceInfo;)V
+Landroid/net/nsd/NsdManager;->DBG:Z
+Landroid/net/nsd/NsdManager;->DISABLE:I
+Landroid/net/nsd/NsdManager;->disconnect()V
+Landroid/net/nsd/NsdManager;->DISCOVER_SERVICES:I
+Landroid/net/nsd/NsdManager;->DISCOVER_SERVICES_FAILED:I
+Landroid/net/nsd/NsdManager;->DISCOVER_SERVICES_STARTED:I
+Landroid/net/nsd/NsdManager;->ENABLE:I
+Landroid/net/nsd/NsdManager;->EVENT_NAMES:Landroid/util/SparseArray;
+Landroid/net/nsd/NsdManager;->fatal(Ljava/lang/String;)V
+Landroid/net/nsd/NsdManager;->FIRST_LISTENER_KEY:I
+Landroid/net/nsd/NsdManager;->getListenerKey(Ljava/lang/Object;)I
+Landroid/net/nsd/NsdManager;->getMessenger()Landroid/os/Messenger;
+Landroid/net/nsd/NsdManager;->getNsdServiceInfoType(Landroid/net/nsd/NsdServiceInfo;)Ljava/lang/String;
+Landroid/net/nsd/NsdManager;->init()V
+Landroid/net/nsd/NsdManager;->mAsyncChannel:Lcom/android/internal/util/AsyncChannel;
+Landroid/net/nsd/NsdManager;->mConnected:Ljava/util/concurrent/CountDownLatch;
+Landroid/net/nsd/NsdManager;->mContext:Landroid/content/Context;
+Landroid/net/nsd/NsdManager;->mHandler:Landroid/net/nsd/NsdManager$ServiceHandler;
+Landroid/net/nsd/NsdManager;->mListenerKey:I
+Landroid/net/nsd/NsdManager;->mListenerMap:Landroid/util/SparseArray;
+Landroid/net/nsd/NsdManager;->mMapLock:Ljava/lang/Object;
+Landroid/net/nsd/NsdManager;->mService:Landroid/net/nsd/INsdManager;
+Landroid/net/nsd/NsdManager;->mServiceMap:Landroid/util/SparseArray;
+Landroid/net/nsd/NsdManager;->nameOf(I)Ljava/lang/String;
+Landroid/net/nsd/NsdManager;->NATIVE_DAEMON_EVENT:I
+Landroid/net/nsd/NsdManager;->nextListenerKey()I
+Landroid/net/nsd/NsdManager;->putListener(Ljava/lang/Object;Landroid/net/nsd/NsdServiceInfo;)I
+Landroid/net/nsd/NsdManager;->REGISTER_SERVICE:I
+Landroid/net/nsd/NsdManager;->REGISTER_SERVICE_FAILED:I
+Landroid/net/nsd/NsdManager;->REGISTER_SERVICE_SUCCEEDED:I
+Landroid/net/nsd/NsdManager;->removeListener(I)V
+Landroid/net/nsd/NsdManager;->RESOLVE_SERVICE:I
+Landroid/net/nsd/NsdManager;->RESOLVE_SERVICE_FAILED:I
+Landroid/net/nsd/NsdManager;->RESOLVE_SERVICE_SUCCEEDED:I
+Landroid/net/nsd/NsdManager;->SERVICE_FOUND:I
+Landroid/net/nsd/NsdManager;->SERVICE_LOST:I
+Landroid/net/nsd/NsdManager;->setEnabled(Z)V
+Landroid/net/nsd/NsdManager;->STOP_DISCOVERY:I
+Landroid/net/nsd/NsdManager;->STOP_DISCOVERY_FAILED:I
+Landroid/net/nsd/NsdManager;->STOP_DISCOVERY_SUCCEEDED:I
+Landroid/net/nsd/NsdManager;->TAG:Ljava/lang/String;
+Landroid/net/nsd/NsdManager;->UNREGISTER_SERVICE:I
+Landroid/net/nsd/NsdManager;->UNREGISTER_SERVICE_FAILED:I
+Landroid/net/nsd/NsdManager;->UNREGISTER_SERVICE_SUCCEEDED:I
+Landroid/net/nsd/NsdServiceInfo;-><init>(Ljava/lang/String;Ljava/lang/String;)V
+Landroid/net/nsd/NsdServiceInfo;->getTxtRecord()[B
+Landroid/net/nsd/NsdServiceInfo;->getTxtRecordSize()I
+Landroid/net/nsd/NsdServiceInfo;->mHost:Ljava/net/InetAddress;
+Landroid/net/nsd/NsdServiceInfo;->mPort:I
+Landroid/net/nsd/NsdServiceInfo;->mServiceName:Ljava/lang/String;
+Landroid/net/nsd/NsdServiceInfo;->mServiceType:Ljava/lang/String;
+Landroid/net/nsd/NsdServiceInfo;->mTxtRecord:Landroid/util/ArrayMap;
+Landroid/net/nsd/NsdServiceInfo;->setTxtRecords(Ljava/lang/String;)V
+Landroid/net/nsd/NsdServiceInfo;->TAG:Ljava/lang/String;
diff --git a/Tethering/apex/hiddenapi/hiddenapi-max-target-o-low-priority.txt b/Tethering/apex/hiddenapi/hiddenapi-max-target-o-low-priority.txt
index ea0f61a..1f49d1b 100644
--- a/Tethering/apex/hiddenapi/hiddenapi-max-target-o-low-priority.txt
+++ b/Tethering/apex/hiddenapi/hiddenapi-max-target-o-low-priority.txt
@@ -1163,93 +1163,6 @@
 Landroid/net/NetworkWatchlistManager;->reportWatchlistIfNecessary()V
 Landroid/net/NetworkWatchlistManager;->SHARED_MEMORY_TAG:Ljava/lang/String;
 Landroid/net/NetworkWatchlistManager;->TAG:Ljava/lang/String;
-Landroid/net/nsd/DnsSdTxtRecord;-><init>()V
-Landroid/net/nsd/DnsSdTxtRecord;-><init>(Landroid/net/nsd/DnsSdTxtRecord;)V
-Landroid/net/nsd/DnsSdTxtRecord;-><init>([B)V
-Landroid/net/nsd/DnsSdTxtRecord;->contains(Ljava/lang/String;)Z
-Landroid/net/nsd/DnsSdTxtRecord;->CREATOR:Landroid/os/Parcelable$Creator;
-Landroid/net/nsd/DnsSdTxtRecord;->get(Ljava/lang/String;)Ljava/lang/String;
-Landroid/net/nsd/DnsSdTxtRecord;->getKey(I)Ljava/lang/String;
-Landroid/net/nsd/DnsSdTxtRecord;->getRawData()[B
-Landroid/net/nsd/DnsSdTxtRecord;->getValue(I)[B
-Landroid/net/nsd/DnsSdTxtRecord;->getValue(Ljava/lang/String;)[B
-Landroid/net/nsd/DnsSdTxtRecord;->getValueAsString(I)Ljava/lang/String;
-Landroid/net/nsd/DnsSdTxtRecord;->insert([B[BI)V
-Landroid/net/nsd/DnsSdTxtRecord;->keyCount()I
-Landroid/net/nsd/DnsSdTxtRecord;->mData:[B
-Landroid/net/nsd/DnsSdTxtRecord;->mSeperator:B
-Landroid/net/nsd/DnsSdTxtRecord;->remove(Ljava/lang/String;)I
-Landroid/net/nsd/DnsSdTxtRecord;->set(Ljava/lang/String;Ljava/lang/String;)V
-Landroid/net/nsd/DnsSdTxtRecord;->size()I
-Landroid/net/nsd/INsdManager$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
-Landroid/net/nsd/INsdManager$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
-Landroid/net/nsd/INsdManager$Stub$Proxy;->getMessenger()Landroid/os/Messenger;
-Landroid/net/nsd/INsdManager$Stub$Proxy;->mRemote:Landroid/os/IBinder;
-Landroid/net/nsd/INsdManager$Stub$Proxy;->setEnabled(Z)V
-Landroid/net/nsd/INsdManager$Stub;-><init>()V
-Landroid/net/nsd/INsdManager$Stub;->DESCRIPTOR:Ljava/lang/String;
-Landroid/net/nsd/INsdManager$Stub;->TRANSACTION_getMessenger:I
-Landroid/net/nsd/INsdManager$Stub;->TRANSACTION_setEnabled:I
-Landroid/net/nsd/INsdManager;->setEnabled(Z)V
-Landroid/net/nsd/NsdManager;-><init>(Landroid/content/Context;Landroid/net/nsd/INsdManager;)V
-Landroid/net/nsd/NsdManager;->BASE:I
-Landroid/net/nsd/NsdManager;->checkListener(Ljava/lang/Object;)V
-Landroid/net/nsd/NsdManager;->checkProtocol(I)V
-Landroid/net/nsd/NsdManager;->checkServiceInfo(Landroid/net/nsd/NsdServiceInfo;)V
-Landroid/net/nsd/NsdManager;->DBG:Z
-Landroid/net/nsd/NsdManager;->DISABLE:I
-Landroid/net/nsd/NsdManager;->disconnect()V
-Landroid/net/nsd/NsdManager;->DISCOVER_SERVICES:I
-Landroid/net/nsd/NsdManager;->DISCOVER_SERVICES_FAILED:I
-Landroid/net/nsd/NsdManager;->DISCOVER_SERVICES_STARTED:I
-Landroid/net/nsd/NsdManager;->ENABLE:I
-Landroid/net/nsd/NsdManager;->EVENT_NAMES:Landroid/util/SparseArray;
-Landroid/net/nsd/NsdManager;->fatal(Ljava/lang/String;)V
-Landroid/net/nsd/NsdManager;->FIRST_LISTENER_KEY:I
-Landroid/net/nsd/NsdManager;->getListenerKey(Ljava/lang/Object;)I
-Landroid/net/nsd/NsdManager;->getMessenger()Landroid/os/Messenger;
-Landroid/net/nsd/NsdManager;->getNsdServiceInfoType(Landroid/net/nsd/NsdServiceInfo;)Ljava/lang/String;
-Landroid/net/nsd/NsdManager;->init()V
-Landroid/net/nsd/NsdManager;->mAsyncChannel:Lcom/android/internal/util/AsyncChannel;
-Landroid/net/nsd/NsdManager;->mConnected:Ljava/util/concurrent/CountDownLatch;
-Landroid/net/nsd/NsdManager;->mContext:Landroid/content/Context;
-Landroid/net/nsd/NsdManager;->mHandler:Landroid/net/nsd/NsdManager$ServiceHandler;
-Landroid/net/nsd/NsdManager;->mListenerKey:I
-Landroid/net/nsd/NsdManager;->mListenerMap:Landroid/util/SparseArray;
-Landroid/net/nsd/NsdManager;->mMapLock:Ljava/lang/Object;
-Landroid/net/nsd/NsdManager;->mService:Landroid/net/nsd/INsdManager;
-Landroid/net/nsd/NsdManager;->mServiceMap:Landroid/util/SparseArray;
-Landroid/net/nsd/NsdManager;->nameOf(I)Ljava/lang/String;
-Landroid/net/nsd/NsdManager;->NATIVE_DAEMON_EVENT:I
-Landroid/net/nsd/NsdManager;->nextListenerKey()I
-Landroid/net/nsd/NsdManager;->putListener(Ljava/lang/Object;Landroid/net/nsd/NsdServiceInfo;)I
-Landroid/net/nsd/NsdManager;->REGISTER_SERVICE:I
-Landroid/net/nsd/NsdManager;->REGISTER_SERVICE_FAILED:I
-Landroid/net/nsd/NsdManager;->REGISTER_SERVICE_SUCCEEDED:I
-Landroid/net/nsd/NsdManager;->removeListener(I)V
-Landroid/net/nsd/NsdManager;->RESOLVE_SERVICE:I
-Landroid/net/nsd/NsdManager;->RESOLVE_SERVICE_FAILED:I
-Landroid/net/nsd/NsdManager;->RESOLVE_SERVICE_SUCCEEDED:I
-Landroid/net/nsd/NsdManager;->SERVICE_FOUND:I
-Landroid/net/nsd/NsdManager;->SERVICE_LOST:I
-Landroid/net/nsd/NsdManager;->setEnabled(Z)V
-Landroid/net/nsd/NsdManager;->STOP_DISCOVERY:I
-Landroid/net/nsd/NsdManager;->STOP_DISCOVERY_FAILED:I
-Landroid/net/nsd/NsdManager;->STOP_DISCOVERY_SUCCEEDED:I
-Landroid/net/nsd/NsdManager;->TAG:Ljava/lang/String;
-Landroid/net/nsd/NsdManager;->UNREGISTER_SERVICE:I
-Landroid/net/nsd/NsdManager;->UNREGISTER_SERVICE_FAILED:I
-Landroid/net/nsd/NsdManager;->UNREGISTER_SERVICE_SUCCEEDED:I
-Landroid/net/nsd/NsdServiceInfo;-><init>(Ljava/lang/String;Ljava/lang/String;)V
-Landroid/net/nsd/NsdServiceInfo;->getTxtRecord()[B
-Landroid/net/nsd/NsdServiceInfo;->getTxtRecordSize()I
-Landroid/net/nsd/NsdServiceInfo;->mHost:Ljava/net/InetAddress;
-Landroid/net/nsd/NsdServiceInfo;->mPort:I
-Landroid/net/nsd/NsdServiceInfo;->mServiceName:Ljava/lang/String;
-Landroid/net/nsd/NsdServiceInfo;->mServiceType:Ljava/lang/String;
-Landroid/net/nsd/NsdServiceInfo;->mTxtRecord:Landroid/util/ArrayMap;
-Landroid/net/nsd/NsdServiceInfo;->setTxtRecords(Ljava/lang/String;)V
-Landroid/net/nsd/NsdServiceInfo;->TAG:Ljava/lang/String;
 Landroid/net/ProxyInfo;-><init>(Landroid/net/ProxyInfo;)V
 Landroid/net/ProxyInfo;-><init>(Landroid/net/Uri;)V
 Landroid/net/ProxyInfo;-><init>(Landroid/net/Uri;I)V
diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags
index f62df7f..6735317 100644
--- a/Tethering/proguard.flags
+++ b/Tethering/proguard.flags
@@ -8,6 +8,10 @@
     native <methods>;
 }
 
+-keep class com.android.networkstack.tethering.util.TcUtils {
+    native <methods>;
+}
+
 -keepclassmembers public class * extends com.android.networkstack.tethering.util.Struct {
     *;
 }
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index b4228da..2bb19db 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -614,10 +614,8 @@
             return false;
         }
 
-        if (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) {
-            // BT configures the interface elsewhere: only start DHCP.
-            // TODO: make all tethering types behave the same way, and delete the bluetooth
-            // code that calls into NetworkManagementService directly.
+        if (shouldNotConfigureBluetoothInterface()) {
+            // Interface was already configured elsewhere, only start DHCP.
             return configureDhcp(enabled, mIpv4Address, null /* clientAddress */);
         }
 
@@ -651,12 +649,15 @@
         return configureDhcp(enabled, mIpv4Address, mStaticIpv4ClientAddr);
     }
 
+    private boolean shouldNotConfigureBluetoothInterface() {
+        // Before T, bluetooth tethering configures the interface elsewhere.
+        return (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) && !SdkLevel.isAtLeastT();
+    }
+
     private LinkAddress requestIpv4Address(final boolean useLastAddress) {
         if (mStaticIpv4ServerAddr != null) return mStaticIpv4ServerAddr;
 
-        if (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) {
-            return new LinkAddress(BLUETOOTH_IFACE_ADDR);
-        }
+        if (shouldNotConfigureBluetoothInterface()) return new LinkAddress(BLUETOOTH_IFACE_ADDR);
 
         return mPrivateAddressCoordinator.requestDownstreamAddress(this, useLastAddress);
     }
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfUtils.java b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java
index ad410eb..77efb51 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfUtils.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java
@@ -55,11 +55,11 @@
     static final boolean DOWNSTREAM = true;
     static final boolean UPSTREAM = false;
 
-    // The priority of clat/tether hooks - smaller is higher priority.
+    // The priority of 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;
+    // Sync from system/netd/server/TcUtils.h.
+    static final short PRIO_TETHER6 = 2;
+    static final short PRIO_TETHER4 = 3;
     // note that the above must be lower than PRIO_CLAT from netd's OffloadUtils.cpp
 
     private static String makeProgPath(boolean downstream, int ipVersion, boolean ether) {
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 55c24d3..db9a64f 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -134,7 +134,12 @@
 import com.android.internal.util.MessageUtils;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
+import com.android.networkstack.apishim.common.BluetoothPanShim;
+import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
+import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim;
+import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
 import com.android.networkstack.tethering.util.InterfaceSet;
 import com.android.networkstack.tethering.util.PrefixUtils;
 import com.android.networkstack.tethering.util.TetheringUtils;
@@ -265,8 +270,11 @@
     private int mOffloadStatus = TETHER_HARDWARE_OFFLOAD_STOPPED;
 
     private EthernetManager.TetheredInterfaceRequest mEthernetIfaceRequest;
+    private TetheredInterfaceRequestShim mBluetoothIfaceRequest;
     private String mConfiguredEthernetIface;
+    private String mConfiguredBluetoothIface;
     private EthernetCallback mEthernetCallback;
+    private TetheredInterfaceCallbackShim mBluetoothCallback;
     private SettingsObserver mSettingsObserver;
     private BluetoothPan mBluetoothPan;
     private PanServiceListener mBluetoothPanListener;
@@ -533,14 +541,16 @@
         }
     }
 
-    // This method needs to exist because TETHERING_BLUETOOTH and TETHERING_WIGIG can't use
-    // enableIpServing.
+    // This method needs to exist because TETHERING_BLUETOOTH before Android T and TETHERING_WIGIG
+    // can't use enableIpServing.
     private void processInterfaceStateChange(final String iface, boolean enabled) {
         // Do not listen to USB interface state changes or USB interface add/removes. USB tethering
         // is driven only by USB_ACTION broadcasts.
         final int type = ifaceNameToType(iface);
         if (type == TETHERING_USB || type == TETHERING_NCM) return;
 
+        if (type == TETHERING_BLUETOOTH && SdkLevel.isAtLeastT()) return;
+
         if (enabled) {
             ensureIpServerStarted(iface);
         } else {
@@ -769,6 +779,9 @@
                             TETHERING_BLUETOOTH);
                 }
                 mPendingPanRequests.clear();
+                mBluetoothIfaceRequest = null;
+                mBluetoothCallback = null;
+                maybeDisableBluetoothIpServing();
             });
         }
 
@@ -779,7 +792,11 @@
 
     private void setBluetoothTetheringSettings(@NonNull final BluetoothPan bluetoothPan,
             final boolean enable, final IIntResultListener listener) {
-        bluetoothPan.setBluetoothTethering(enable);
+        if (SdkLevel.isAtLeastT()) {
+            changeBluetoothTetheringSettings(bluetoothPan, enable);
+        } else {
+            changeBluetoothTetheringSettingsPreT(bluetoothPan, enable);
+        }
 
         // Enabling bluetooth tethering settings can silently fail. Send internal error if the
         // result is not expected.
@@ -788,6 +805,68 @@
         sendTetherResult(listener, result, TETHERING_BLUETOOTH);
     }
 
+    private void changeBluetoothTetheringSettingsPreT(@NonNull final BluetoothPan bluetoothPan,
+            final boolean enable) {
+        bluetoothPan.setBluetoothTethering(enable);
+    }
+
+    private void changeBluetoothTetheringSettings(@NonNull final BluetoothPan bluetoothPan,
+            final boolean enable) {
+        final BluetoothPanShim panShim = mDeps.getBluetoothPanShim(bluetoothPan);
+        if (enable) {
+            if (mBluetoothIfaceRequest != null) {
+                Log.d(TAG, "Bluetooth tethering settings already enabled");
+                return;
+            }
+
+            mBluetoothCallback = new BluetoothCallback();
+            try {
+                mBluetoothIfaceRequest = panShim.requestTetheredInterface(mExecutor,
+                        mBluetoothCallback);
+            } catch (UnsupportedApiLevelException e) {
+                Log.wtf(TAG, "Use unsupported API, " + e);
+            }
+        } else {
+            if (mBluetoothIfaceRequest == null) {
+                Log.d(TAG, "Bluetooth tethering settings already disabled");
+                return;
+            }
+
+            mBluetoothIfaceRequest.release();
+            mBluetoothIfaceRequest = null;
+            mBluetoothCallback = null;
+            // If bluetooth request is released, tethering won't able to receive
+            // onUnavailable callback, explicitly disable bluetooth IpServer manually.
+            maybeDisableBluetoothIpServing();
+        }
+    }
+
+    // BluetoothCallback is only called after T. Before T, PanService would call tether/untether to
+    // notify bluetooth interface status.
+    private class BluetoothCallback implements TetheredInterfaceCallbackShim {
+        @Override
+        public void onAvailable(String iface) {
+            if (this != mBluetoothCallback) return;
+
+            enableIpServing(TETHERING_BLUETOOTH, iface, getRequestedState(TETHERING_BLUETOOTH));
+            mConfiguredBluetoothIface = iface;
+        }
+
+        @Override
+        public void onUnavailable() {
+            if (this != mBluetoothCallback) return;
+
+            maybeDisableBluetoothIpServing();
+        }
+    }
+
+    private void maybeDisableBluetoothIpServing() {
+        if (mConfiguredBluetoothIface == null) return;
+
+        ensureIpServerStopped(mConfiguredBluetoothIface);
+        mConfiguredBluetoothIface = null;
+    }
+
     private int setEthernetTethering(final boolean enable) {
         final EthernetManager em = (EthernetManager) mContext.getSystemService(
                 Context.ETHERNET_SERVICE);
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 7df9475..c1a747e 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -18,6 +18,7 @@
 
 import android.app.usage.NetworkStatsManager;
 import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothPan;
 import android.content.Context;
 import android.net.INetd;
 import android.net.ip.IpServer;
@@ -31,6 +32,8 @@
 import androidx.annotation.NonNull;
 
 import com.android.internal.util.StateMachine;
+import com.android.networkstack.apishim.BluetoothPanShimImpl;
+import com.android.networkstack.apishim.common.BluetoothPanShim;
 
 import java.util.ArrayList;
 
@@ -158,4 +161,13 @@
             TetheringConfiguration cfg) {
         return new PrivateAddressCoordinator(ctx, cfg);
     }
+
+    /**
+     * Get BluetoothPanShim object to enable/disable bluetooth tethering.
+     *
+     * TODO: use BluetoothPan directly when mainline module is built with API 32.
+     */
+    public BluetoothPanShim getBluetoothPanShim(BluetoothPan pan) {
+        return BluetoothPanShimImpl.newInstance(pan);
+    }
 }
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 2f2cde0..267c376 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -16,6 +16,7 @@
 
 package android.net.ip;
 
+import static android.net.INetd.IF_STATE_DOWN;
 import static android.net.INetd.IF_STATE_UP;
 import static android.net.RouteInfo.RTN_UNICAST;
 import static android.net.TetheringManager.TETHERING_BLUETOOTH;
@@ -33,6 +34,7 @@
 import static android.net.ip.IpServer.STATE_UNAVAILABLE;
 import static android.system.OsConstants.ETH_P_IPV6;
 
+import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_DELNEIGH;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWNEIGH;
@@ -400,11 +402,16 @@
     }
 
     @Test
-    public void canBeTethered() throws Exception {
+    public void canBeTetheredAsBluetooth() throws Exception {
         initStateMachine(TETHERING_BLUETOOTH);
 
         dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
-        InOrder inOrder = inOrder(mCallback, mNetd);
+        InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator);
+        if (isAtLeastT()) {
+            inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true));
+            inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
+                    IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP)));
+        }
         inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME);
         inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
         // One for ipv4 route, one for ipv6 link local route.
@@ -426,7 +433,13 @@
         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)));
+        // One is ipv4 address clear (set to 0.0.0.0), another is set interface down which only
+        // happen after T. Before T, the interface configuration control in bluetooth side.
+        if (isAtLeastT()) {
+            inOrder.verify(mNetd).interfaceSetCfg(
+                    argThat(cfg -> assertContainsFlag(cfg.flags, IF_STATE_DOWN)));
+        }
+        inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> cfg.flags.length == 0));
         inOrder.verify(mAddressCoordinator).releaseDownstream(any());
         inOrder.verify(mCallback).updateInterfaceState(
                 mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
@@ -443,7 +456,7 @@
         InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator);
         inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true));
         inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
-                  IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP)));
+                IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP)));
         inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME);
         inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
         inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME),
@@ -587,7 +600,8 @@
         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(mNetd, times(isAtLeastT() ? 2 : 1)).interfaceSetCfg(
+                argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
         inOrder.verify(mAddressCoordinator).releaseDownstream(any());
         inOrder.verify(mBpfCoordinator).tetherOffloadClientClear(mIpServer);
         inOrder.verify(mBpfCoordinator).stopMonitoring(mIpServer);
@@ -683,7 +697,11 @@
         initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE);
         dispatchTetherConnectionChanged(UPSTREAM_IFACE);
 
-        assertDhcpStarted(mBluetoothPrefix);
+        if (isAtLeastT()) {
+            assertDhcpStarted(PrefixUtils.asIpPrefix(mTestAddress));
+        } else {
+            assertDhcpStarted(mBluetoothPrefix);
+        }
     }
 
     @Test
@@ -1371,7 +1389,6 @@
         for (String flag : flags) {
             if (flag.equals(match)) return true;
         }
-        fail("Missing flag: " + match);
         return false;
     }
 
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 40d133a..e4dbc7d 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -62,6 +62,7 @@
 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 
 import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
 import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH;
 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
 import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_0;
@@ -81,6 +82,8 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.notNull;
 import static org.mockito.Matchers.anyInt;
@@ -182,6 +185,10 @@
 import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.net.module.util.CollectionUtils;
+import com.android.networkstack.apishim.common.BluetoothPanShim;
+import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
+import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim;
+import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
 import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent;
 import com.android.testutils.MiscAsserts;
 
@@ -261,6 +268,8 @@
     @Mock private PackageManager mPackageManager;
     @Mock private BluetoothAdapter mBluetoothAdapter;
     @Mock private BluetoothPan mBluetoothPan;
+    @Mock private BluetoothPanShim mBluetoothPanShim;
+    @Mock private TetheredInterfaceRequestShim mTetheredInterfaceRequestShim;
 
     private final MockIpServerDependencies mIpServerDependencies =
             spy(new MockIpServerDependencies());
@@ -285,6 +294,7 @@
     private PrivateAddressCoordinator mPrivateAddressCoordinator;
     private SoftApCallback mSoftApCallback;
     private UpstreamNetworkMonitor mUpstreamNetworkMonitor;
+    private TetheredInterfaceCallbackShim mTetheredInterfaceCallbackShim;
 
     private TestConnectivityManager mCm;
 
@@ -483,13 +493,23 @@
             return false;
         }
 
-
         @Override
         public PrivateAddressCoordinator getPrivateAddressCoordinator(Context ctx,
                 TetheringConfiguration cfg) {
             mPrivateAddressCoordinator = super.getPrivateAddressCoordinator(ctx, cfg);
             return mPrivateAddressCoordinator;
         }
+
+        @Override
+        public BluetoothPanShim getBluetoothPanShim(BluetoothPan pan) {
+            try {
+                when(mBluetoothPanShim.requestTetheredInterface(
+                        any(), any())).thenReturn(mTetheredInterfaceRequestShim);
+            } catch (UnsupportedApiLevelException e) {
+                fail("BluetoothPan#requestTetheredInterface is not supported");
+            }
+            return mBluetoothPanShim;
+        }
     }
 
     private static LinkProperties buildUpstreamLinkProperties(String interfaceName,
@@ -2557,6 +2577,44 @@
 
     @Test
     public void testBluetoothTethering() throws Exception {
+        // Switch to @IgnoreUpTo(Build.VERSION_CODES.S_V2) when it is available for AOSP.
+        assumeTrue(isAtLeastT());
+
+        final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
+        mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
+        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result);
+        mLooper.dispatchAll();
+        verifySetBluetoothTethering(true /* enable */, true /* bindToPanService */);
+        result.assertHasResult();
+
+        mTetheredInterfaceCallbackShim.onAvailable(TEST_BT_IFNAME);
+        mLooper.dispatchAll();
+        verifyNetdCommandForBtSetup();
+
+        // If PAN disconnect, tethering should also be stopped.
+        mTetheredInterfaceCallbackShim.onUnavailable();
+        mLooper.dispatchAll();
+        verifyNetdCommandForBtTearDown();
+
+        // Tethering could restart if PAN reconnect.
+        mTetheredInterfaceCallbackShim.onAvailable(TEST_BT_IFNAME);
+        mLooper.dispatchAll();
+        verifyNetdCommandForBtSetup();
+
+        // Pretend that bluetooth tethering was disabled.
+        mockBluetoothSettings(true /* bluetoothOn */, false /* tetheringOn */);
+        mTethering.stopTethering(TETHERING_BLUETOOTH);
+        mLooper.dispatchAll();
+        verifySetBluetoothTethering(false /* enable */, false /* bindToPanService */);
+
+        verifyNetdCommandForBtTearDown();
+    }
+
+    @Test
+    public void testBluetoothTetheringBeforeT() throws Exception {
+        // Switch to @IgnoreAfter(Build.VERSION_CODES.S_V2) when it is available for AOSP.
+        assumeFalse(isAtLeastT());
+
         final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
         mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result);
@@ -2610,12 +2668,17 @@
         mTethering.interfaceAdded(TEST_BT_IFNAME);
         mLooper.dispatchAll();
 
-        mTethering.interfaceStatusChanged(TEST_BT_IFNAME, false);
-        mTethering.interfaceStatusChanged(TEST_BT_IFNAME, true);
-        final ResultListener tetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
-        mTethering.tether(TEST_BT_IFNAME, IpServer.STATE_TETHERED, tetherResult);
-        mLooper.dispatchAll();
-        tetherResult.assertHasResult();
+        if (isAtLeastT()) {
+            mTetheredInterfaceCallbackShim.onAvailable(TEST_BT_IFNAME);
+            mLooper.dispatchAll();
+        } else {
+            mTethering.interfaceStatusChanged(TEST_BT_IFNAME, false);
+            mTethering.interfaceStatusChanged(TEST_BT_IFNAME, true);
+            final ResultListener tetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
+            mTethering.tether(TEST_BT_IFNAME, IpServer.STATE_TETHERED, tetherResult);
+            mLooper.dispatchAll();
+            tetherResult.assertHasResult();
+        }
 
         verifyNetdCommandForBtSetup();
 
@@ -2632,6 +2695,10 @@
     }
 
     private void verifyNetdCommandForBtSetup() throws Exception {
+        if (isAtLeastT()) {
+            verify(mNetd).interfaceSetCfg(argThat(cfg -> TEST_BT_IFNAME.equals(cfg.ifName)
+                    && assertContainsFlag(cfg.flags, INetd.IF_STATE_UP)));
+        }
         verify(mNetd).tetherInterfaceAdd(TEST_BT_IFNAME);
         verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME);
         verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_BT_IFNAME),
@@ -2644,19 +2711,30 @@
         reset(mNetd);
     }
 
+    private boolean assertContainsFlag(String[] flags, String match) {
+        for (String flag : flags) {
+            if (flag.equals(match)) return true;
+        }
+        return false;
+    }
+
     private void verifyNetdCommandForBtTearDown() throws Exception {
         verify(mNetd).tetherApplyDnsInterfaces();
         verify(mNetd).tetherInterfaceRemove(TEST_BT_IFNAME);
         verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME);
-        verify(mNetd).interfaceSetCfg(any(InterfaceConfigurationParcel.class));
+        // One is ipv4 address clear (set to 0.0.0.0), another is set interface down which only
+        // happen after T. Before T, the interface configuration control in bluetooth side.
+        verify(mNetd, times(isAtLeastT() ? 2 : 1)).interfaceSetCfg(
+                any(InterfaceConfigurationParcel.class));
         verify(mNetd).tetherStop();
         verify(mNetd).ipfwdDisableForwarding(TETHERING_NAME);
+        reset(mNetd);
     }
 
     // If bindToPanService is true, this function would return ServiceListener which could notify
     // PanService is connected or disconnected.
     private ServiceListener verifySetBluetoothTethering(final boolean enable,
-            final boolean bindToPanService) {
+            final boolean bindToPanService) throws Exception {
         ServiceListener listener = null;
         verify(mBluetoothAdapter).isEnabled();
         if (bindToPanService) {
@@ -2671,7 +2749,19 @@
             verify(mBluetoothAdapter, never()).getProfileProxy(eq(mServiceContext), any(),
                     anyInt());
         }
-        verify(mBluetoothPan).setBluetoothTethering(enable);
+
+        if (isAtLeastT()) {
+            if (enable) {
+                final ArgumentCaptor<TetheredInterfaceCallbackShim> callbackCaptor =
+                        ArgumentCaptor.forClass(TetheredInterfaceCallbackShim.class);
+                verify(mBluetoothPanShim).requestTetheredInterface(any(), callbackCaptor.capture());
+                mTetheredInterfaceCallbackShim = callbackCaptor.getValue();
+            } else {
+                verify(mTetheredInterfaceRequestShim).release();
+            }
+        } else {
+            verify(mBluetoothPan).setBluetoothTethering(enable);
+        }
         verify(mBluetoothPan).isTetheringOn();
         verifyNoMoreInteractions(mBluetoothAdapter, mBluetoothPan);
         reset(mBluetoothAdapter, mBluetoothPan);
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index d015ef6..cce2b71 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -42,10 +42,12 @@
         // TODO: remove it when NetworkStatsService is moved into the mainline module and no more
         // calls to JNI in libservices.core.
         "//frameworks/base/services/core/jni",
+        "//packages/modules/Connectivity/service",
+        "//packages/modules/Connectivity/service/native/libs/libclat",
         "//packages/modules/Connectivity/Tethering",
+        "//packages/modules/Connectivity/service/native",
+        "//packages/modules/Connectivity/service-t/native/libs/libnetworkstats",
         "//packages/modules/Connectivity/tests/unit/jni",
-        // TODO: remove system/netd/* when all BPF code is moved out of Netd.
-        "//system/netd/libnetdbpf",
         "//system/netd/server",
         "//system/netd/tests",
     ],
@@ -55,6 +57,16 @@
 // bpf kernel programs
 //
 bpf {
+    name: "dscp_policy.o",
+    srcs: ["dscp_policy.c"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+    sub_dir: "net_shared",
+}
+
+bpf {
     name: "offload.o",
     srcs: ["offload.c"],
     cflags: [
@@ -71,3 +83,29 @@
         "-Werror",
     ],
 }
+
+bpf {
+    name: "clatd.o_mainline",
+    srcs: ["clatd.c"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+    include_dirs: [
+        "frameworks/libs/net/common/netd/libnetdutils/include",
+    ],
+    sub_dir: "net_shared",
+}
+
+bpf {
+    name: "netd.o_mainline",
+    srcs: ["netd.c"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+    include_dirs: [
+        "frameworks/libs/net/common/netd/libnetdutils/include",
+    ],
+    sub_dir: "net_shared",
+}
diff --git a/bpf_progs/bpf_shared.h b/bpf_progs/bpf_shared.h
index 8577d9d..f0df97b 100644
--- a/bpf_progs/bpf_shared.h
+++ b/bpf_progs/bpf_shared.h
@@ -20,7 +20,6 @@
 #include <linux/if_ether.h>
 #include <linux/in.h>
 #include <linux/in6.h>
-#include <netdutils/UidConstants.h>
 
 // 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.
diff --git a/bpf_progs/clatd.c b/bpf_progs/clatd.c
new file mode 100644
index 0000000..dc646c3
--- /dev/null
+++ b/bpf_progs/clatd.c
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2019 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 <linux/bpf.h>
+#include <linux/if.h>
+#include <linux/if_ether.h>
+#include <linux/in.h>
+#include <linux/in6.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/pkt_cls.h>
+#include <linux/swab.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+// bionic kernel uapi linux/udp.h header is munged...
+#define __kernel_udphdr udphdr
+#include <linux/udp.h>
+
+#include "bpf_helpers.h"
+#include "bpf_net_helpers.h"
+#include "bpf_shared.h"
+
+// From kernel:include/net/ip.h
+#define IP_DF 0x4000  // Flag: "Don't Fragment"
+
+DEFINE_BPF_MAP_GRW(clat_ingress6_map, HASH, ClatIngress6Key, ClatIngress6Value, 16, AID_SYSTEM)
+
+static inline __always_inline int nat64(struct __sk_buff* skb, bool is_ethernet) {
+    const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0;
+    void* data = (void*)(long)skb->data;
+    const void* data_end = (void*)(long)skb->data_end;
+    const struct ethhdr* const eth = is_ethernet ? data : NULL;  // used iff is_ethernet
+    const struct ipv6hdr* const ip6 = is_ethernet ? (void*)(eth + 1) : data;
+
+    // Require ethernet dst mac address to be our unicast address.
+    if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_PIPE;
+
+    // Must be meta-ethernet IPv6 frame
+    if (skb->protocol != htons(ETH_P_IPV6)) return TC_ACT_PIPE;
+
+    // Must have (ethernet and) ipv6 header
+    if (data + l2_header_size + sizeof(*ip6) > data_end) return TC_ACT_PIPE;
+
+    // Ethertype - if present - must be IPv6
+    if (is_ethernet && (eth->h_proto != htons(ETH_P_IPV6))) return TC_ACT_PIPE;
+
+    // IP version must be 6
+    if (ip6->version != 6) return TC_ACT_PIPE;
+
+    // Maximum IPv6 payload length that can be translated to IPv4
+    if (ntohs(ip6->payload_len) > 0xFFFF - sizeof(struct iphdr)) return TC_ACT_PIPE;
+
+    switch (ip6->nexthdr) {
+        case IPPROTO_TCP:  // For TCP & UDP the checksum neutrality of the chosen IPv6
+        case IPPROTO_UDP:  // address means there is no need to update their checksums.
+        case IPPROTO_GRE:  // We do not need to bother looking at GRE/ESP headers,
+        case IPPROTO_ESP:  // since there is never a checksum to update.
+            break;
+
+        default:  // do not know how to handle anything else
+            return TC_ACT_PIPE;
+    }
+
+    ClatIngress6Key k = {
+            .iif = skb->ifindex,
+            .pfx96.in6_u.u6_addr32 =
+                    {
+                            ip6->saddr.in6_u.u6_addr32[0],
+                            ip6->saddr.in6_u.u6_addr32[1],
+                            ip6->saddr.in6_u.u6_addr32[2],
+                    },
+            .local6 = ip6->daddr,
+    };
+
+    ClatIngress6Value* v = bpf_clat_ingress6_map_lookup_elem(&k);
+
+    if (!v) return TC_ACT_PIPE;
+
+    struct ethhdr eth2;  // used iff is_ethernet
+    if (is_ethernet) {
+        eth2 = *eth;                     // Copy over the ethernet header (src/dst mac)
+        eth2.h_proto = htons(ETH_P_IP);  // But replace the ethertype
+    }
+
+    struct iphdr ip = {
+            .version = 4,                                                      // u4
+            .ihl = sizeof(struct iphdr) / sizeof(__u32),                       // u4
+            .tos = (ip6->priority << 4) + (ip6->flow_lbl[0] >> 4),             // u8
+            .tot_len = htons(ntohs(ip6->payload_len) + sizeof(struct iphdr)),  // u16
+            .id = 0,                                                           // u16
+            .frag_off = htons(IP_DF),                                          // u16
+            .ttl = ip6->hop_limit,                                             // u8
+            .protocol = ip6->nexthdr,                                          // u8
+            .check = 0,                                                        // u16
+            .saddr = ip6->saddr.in6_u.u6_addr32[3],                            // u32
+            .daddr = v->local4.s_addr,                                         // u32
+    };
+
+    // Calculate the IPv4 one's complement checksum of the IPv4 header.
+    __wsum sum4 = 0;
+    for (int i = 0; i < sizeof(ip) / sizeof(__u16); ++i) {
+        sum4 += ((__u16*)&ip)[i];
+    }
+    // Note that sum4 is guaranteed to be non-zero by virtue of ip.version == 4
+    sum4 = (sum4 & 0xFFFF) + (sum4 >> 16);  // collapse u32 into range 1 .. 0x1FFFE
+    sum4 = (sum4 & 0xFFFF) + (sum4 >> 16);  // collapse any potential carry into u16
+    ip.check = (__u16)~sum4;                // sum4 cannot be zero, so this is never 0xFFFF
+
+    // Calculate the *negative* IPv6 16-bit one's complement checksum of the IPv6 header.
+    __wsum sum6 = 0;
+    // We'll end up with a non-zero sum due to ip6->version == 6 (which has '0' bits)
+    for (int i = 0; i < sizeof(*ip6) / sizeof(__u16); ++i) {
+        sum6 += ~((__u16*)ip6)[i];  // note the bitwise negation
+    }
+
+    // Note that there is no L4 checksum update: we are relying on the checksum neutrality
+    // of the ipv6 address chosen by netd's ClatdController.
+
+    // Packet mutations begin - point of no return, but if this first modification fails
+    // the packet is probably still pristine, so let clatd handle it.
+    if (bpf_skb_change_proto(skb, htons(ETH_P_IP), 0)) return TC_ACT_PIPE;
+
+    // This takes care of updating the skb->csum field for a CHECKSUM_COMPLETE packet.
+    //
+    // In such a case, skb->csum is a 16-bit one's complement sum of the entire payload,
+    // thus we need to subtract out the ipv6 header's sum, and add in the ipv4 header's sum.
+    // However, by construction of ip.check above the checksum of an ipv4 header is zero.
+    // Thus we only need to subtract the ipv6 header's sum, which is the same as adding
+    // in the sum of the bitwise negation of the ipv6 header.
+    //
+    // bpf_csum_update() always succeeds if the skb is CHECKSUM_COMPLETE and returns an error
+    // (-ENOTSUPP) if it isn't.  So we just ignore the return code.
+    //
+    // if (skb->ip_summed == CHECKSUM_COMPLETE)
+    //   return (skb->csum = csum_add(skb->csum, csum));
+    // else
+    //   return -ENOTSUPP;
+    bpf_csum_update(skb, sum6);
+
+    // bpf_skb_change_proto() invalidates all pointers - reload them.
+    data = (void*)(long)skb->data;
+    data_end = (void*)(long)skb->data_end;
+
+    // I cannot think of any valid way for this error condition to trigger, however I do
+    // believe the explicit check is required to keep the in kernel ebpf verifier happy.
+    if (data + l2_header_size + sizeof(struct iphdr) > data_end) return TC_ACT_SHOT;
+
+    if (is_ethernet) {
+        struct ethhdr* new_eth = data;
+
+        // Copy over the updated ethernet header
+        *new_eth = eth2;
+
+        // Copy over the new ipv4 header.
+        *(struct iphdr*)(new_eth + 1) = ip;
+    } else {
+        // Copy over the new ipv4 header without an ethernet header.
+        *(struct iphdr*)data = ip;
+    }
+
+    // Redirect, possibly back to same interface, so tcpdump sees packet twice.
+    if (v->oif) return bpf_redirect(v->oif, BPF_F_INGRESS);
+
+    // Just let it through, tcpdump will not see IPv4 packet.
+    return TC_ACT_PIPE;
+}
+
+DEFINE_BPF_PROG("schedcls/ingress6/clat_ether", AID_ROOT, AID_SYSTEM, sched_cls_ingress6_clat_ether)
+(struct __sk_buff* skb) {
+    return nat64(skb, true);
+}
+
+DEFINE_BPF_PROG("schedcls/ingress6/clat_rawip", AID_ROOT, AID_SYSTEM, sched_cls_ingress6_clat_rawip)
+(struct __sk_buff* skb) {
+    return nat64(skb, false);
+}
+
+DEFINE_BPF_MAP_GRW(clat_egress4_map, HASH, ClatEgress4Key, ClatEgress4Value, 16, AID_SYSTEM)
+
+DEFINE_BPF_PROG("schedcls/egress4/clat_ether", AID_ROOT, AID_SYSTEM, sched_cls_egress4_clat_ether)
+(struct __sk_buff* skb) {
+    return TC_ACT_PIPE;
+}
+
+DEFINE_BPF_PROG("schedcls/egress4/clat_rawip", AID_ROOT, AID_SYSTEM, sched_cls_egress4_clat_rawip)
+(struct __sk_buff* skb) {
+    void* data = (void*)(long)skb->data;
+    const void* data_end = (void*)(long)skb->data_end;
+    const struct iphdr* const ip4 = data;
+
+    // Must be meta-ethernet IPv4 frame
+    if (skb->protocol != htons(ETH_P_IP)) return TC_ACT_PIPE;
+
+    // Must have ipv4 header
+    if (data + sizeof(*ip4) > data_end) return TC_ACT_PIPE;
+
+    // IP version must be 4
+    if (ip4->version != 4) return TC_ACT_PIPE;
+
+    // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
+    if (ip4->ihl != 5) return TC_ACT_PIPE;
+
+    // Calculate the IPv4 one's complement checksum of the IPv4 header.
+    __wsum sum4 = 0;
+    for (int i = 0; i < sizeof(*ip4) / sizeof(__u16); ++i) {
+        sum4 += ((__u16*)ip4)[i];
+    }
+    // Note that sum4 is guaranteed to be non-zero by virtue of ip4->version == 4
+    sum4 = (sum4 & 0xFFFF) + (sum4 >> 16);  // collapse u32 into range 1 .. 0x1FFFE
+    sum4 = (sum4 & 0xFFFF) + (sum4 >> 16);  // collapse any potential carry into u16
+    // for a correct checksum we should get *a* zero, but sum4 must be positive, ie 0xFFFF
+    if (sum4 != 0xFFFF) return TC_ACT_PIPE;
+
+    // Minimum IPv4 total length is the size of the header
+    if (ntohs(ip4->tot_len) < sizeof(*ip4)) return TC_ACT_PIPE;
+
+    // We are incapable of dealing with IPv4 fragments
+    if (ip4->frag_off & ~htons(IP_DF)) return TC_ACT_PIPE;
+
+    switch (ip4->protocol) {
+        case IPPROTO_TCP:  // For TCP & UDP the checksum neutrality of the chosen IPv6
+        case IPPROTO_GRE:  // address means there is no need to update their checksums.
+        case IPPROTO_ESP:  // We do not need to bother looking at GRE/ESP headers,
+            break;         // since there is never a checksum to update.
+
+        case IPPROTO_UDP:  // See above comment, but must also have UDP header...
+            if (data + sizeof(*ip4) + sizeof(struct udphdr) > data_end) return TC_ACT_PIPE;
+            const struct udphdr* uh = (const struct udphdr*)(ip4 + 1);
+            // If IPv4/UDP checksum is 0 then fallback to clatd so it can calculate the
+            // checksum.  Otherwise the network or more likely the NAT64 gateway might
+            // drop the packet because in most cases IPv6/UDP packets with a zero checksum
+            // are invalid. See RFC 6935.  TODO: calculate checksum via bpf_csum_diff()
+            if (!uh->check) return TC_ACT_PIPE;
+            break;
+
+        default:  // do not know how to handle anything else
+            return TC_ACT_PIPE;
+    }
+
+    ClatEgress4Key k = {
+            .iif = skb->ifindex,
+            .local4.s_addr = ip4->saddr,
+    };
+
+    ClatEgress4Value* v = bpf_clat_egress4_map_lookup_elem(&k);
+
+    if (!v) return TC_ACT_PIPE;
+
+    // Translating without redirecting doesn't make sense.
+    if (!v->oif) return TC_ACT_PIPE;
+
+    // This implementation is currently limited to rawip.
+    if (v->oifIsEthernet) return TC_ACT_PIPE;
+
+    struct ipv6hdr ip6 = {
+            .version = 6,                                    // __u8:4
+            .priority = ip4->tos >> 4,                       // __u8:4
+            .flow_lbl = {(ip4->tos & 0xF) << 4, 0, 0},       // __u8[3]
+            .payload_len = htons(ntohs(ip4->tot_len) - 20),  // __be16
+            .nexthdr = ip4->protocol,                        // __u8
+            .hop_limit = ip4->ttl,                           // __u8
+            .saddr = v->local6,                              // struct in6_addr
+            .daddr = v->pfx96,                               // struct in6_addr
+    };
+    ip6.daddr.in6_u.u6_addr32[3] = ip4->daddr;
+
+    // Calculate the IPv6 16-bit one's complement checksum of the IPv6 header.
+    __wsum sum6 = 0;
+    // We'll end up with a non-zero sum due to ip6.version == 6
+    for (int i = 0; i < sizeof(ip6) / sizeof(__u16); ++i) {
+        sum6 += ((__u16*)&ip6)[i];
+    }
+
+    // Note that there is no L4 checksum update: we are relying on the checksum neutrality
+    // of the ipv6 address chosen by netd's ClatdController.
+
+    // Packet mutations begin - point of no return, but if this first modification fails
+    // the packet is probably still pristine, so let clatd handle it.
+    if (bpf_skb_change_proto(skb, htons(ETH_P_IPV6), 0)) return TC_ACT_PIPE;
+
+    // This takes care of updating the skb->csum field for a CHECKSUM_COMPLETE packet.
+    //
+    // In such a case, skb->csum is a 16-bit one's complement sum of the entire payload,
+    // thus we need to subtract out the ipv4 header's sum, and add in the ipv6 header's sum.
+    // However, we've already verified the ipv4 checksum is correct and thus 0.
+    // Thus we only need to add the ipv6 header's sum.
+    //
+    // bpf_csum_update() always succeeds if the skb is CHECKSUM_COMPLETE and returns an error
+    // (-ENOTSUPP) if it isn't.  So we just ignore the return code (see above for more details).
+    bpf_csum_update(skb, sum6);
+
+    // bpf_skb_change_proto() invalidates all pointers - reload them.
+    data = (void*)(long)skb->data;
+    data_end = (void*)(long)skb->data_end;
+
+    // I cannot think of any valid way for this error condition to trigger, however I do
+    // believe the explicit check is required to keep the in kernel ebpf verifier happy.
+    if (data + sizeof(ip6) > data_end) return TC_ACT_SHOT;
+
+    // Copy over the new ipv6 header without an ethernet header.
+    *(struct ipv6hdr*)data = ip6;
+
+    // Redirect to non v4-* interface.  Tcpdump only sees packet after this redirect.
+    return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
+}
+
+LICENSE("Apache 2.0");
+CRITICAL("netd");
diff --git a/bpf_progs/dscp_policy.c b/bpf_progs/dscp_policy.c
new file mode 100644
index 0000000..9989e6b
--- /dev/null
+++ b/bpf_progs/dscp_policy.c
@@ -0,0 +1,280 @@
+/*
+ * 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 <linux/types.h>
+#include <linux/bpf.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/if_ether.h>
+#include <linux/pkt_cls.h>
+#include <linux/tcp.h>
+#include <stdint.h>
+#include <netinet/in.h>
+#include <netinet/udp.h>
+#include <string.h>
+
+#include "bpf_helpers.h"
+
+#define MAX_POLICIES 16
+#define MAP_A 1
+#define MAP_B 2
+
+#define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.")
+
+// TODO: these are already defined in /system/netd/bpf_progs/bpf_net_helpers.h
+// should they be moved to common location?
+static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) =
+        (void*)BPF_FUNC_get_socket_cookie;
+static int (*bpf_skb_store_bytes)(struct __sk_buff* skb, __u32 offset, const void* from, __u32 len,
+                                  __u64 flags) = (void*)BPF_FUNC_skb_store_bytes;
+static int (*bpf_l3_csum_replace)(struct __sk_buff* skb, __u32 offset, __u64 from, __u64 to,
+                                  __u64 flags) = (void*)BPF_FUNC_l3_csum_replace;
+
+typedef struct {
+    // Add family here to match __sk_buff ?
+    struct in_addr srcIp;
+    struct in_addr dstIp;
+    __be16 srcPort;
+    __be16 dstPort;
+    uint8_t proto;
+    uint8_t dscpVal;
+    uint8_t pad[2];
+} Ipv4RuleEntry;
+STRUCT_SIZE(Ipv4RuleEntry, 2 * 4 + 2 * 2 + 2 * 1 + 2);  // 16, 4 for in_addr
+
+#define SRC_IP_MASK     1
+#define DST_IP_MASK     2
+#define SRC_PORT_MASK   4
+#define DST_PORT_MASK   8
+#define PROTO_MASK      16
+
+typedef struct {
+    struct in6_addr srcIp;
+    struct in6_addr dstIp;
+    __be16 srcPort;
+    __be16 dstPortStart;
+    __be16 dstPortEnd;
+    uint8_t proto;
+    uint8_t dscpVal;
+    uint8_t mask;
+    uint8_t pad[3];
+} Ipv4Policy;
+STRUCT_SIZE(Ipv4Policy, 2 * 16 + 3 * 2 + 3 * 1 + 3);  // 44
+
+typedef struct {
+    struct in6_addr srcIp;
+    struct in6_addr dstIp;
+    __be16 srcPort;
+    __be16 dstPortStart;
+    __be16 dstPortEnd;
+    uint8_t proto;
+    uint8_t dscpVal;
+    uint8_t mask;
+    // should we override this struct to include the param bitmask for linear search?
+    // For mapping socket to policies, all the params should match exactly since we can
+    // pull any missing from the sock itself.
+} Ipv6RuleEntry;
+STRUCT_SIZE(Ipv6RuleEntry, 2 * 16 + 3 * 2 + 3 * 1 + 3);  // 44
+
+// TODO: move to using 1 map. Map v4 address to 0xffff::v4
+DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_A, HASH, uint64_t, Ipv4RuleEntry, MAX_POLICIES,
+        AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_B, HASH, uint64_t, Ipv4RuleEntry, MAX_POLICIES,
+        AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_A, HASH, uint64_t, Ipv6RuleEntry, MAX_POLICIES,
+        AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_B, HASH, uint64_t, Ipv6RuleEntry, MAX_POLICIES,
+        AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(switch_comp_map, ARRAY, int, uint64_t, 1, AID_SYSTEM)
+
+DEFINE_BPF_MAP_GRW(ipv4_dscp_policies_map, ARRAY, uint32_t, Ipv4Policy, MAX_POLICIES,
+        AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv6_dscp_policies_map, ARRAY, uint32_t, Ipv6RuleEntry, MAX_POLICIES,
+        AID_SYSTEM)
+
+DEFINE_BPF_PROG_KVER("schedcls/set_dscp", AID_ROOT, AID_SYSTEM,
+                     schedcls_set_dscp, KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+    int one = 0;
+    uint64_t* selectedMap = bpf_switch_comp_map_lookup_elem(&one);
+
+    // use this with HASH map so map lookup only happens once policies have been added?
+    if (!selectedMap) {
+        return TC_ACT_PIPE;
+    }
+
+    // used for map lookup
+    uint64_t cookie = bpf_get_socket_cookie(skb);
+
+    // Do we need separate maps for ipv4/ipv6
+    if (skb->protocol == htons(ETH_P_IP)) { //maybe bpf_htons()
+        Ipv4RuleEntry* v4Policy;
+        if (*selectedMap == MAP_A) {
+            v4Policy = bpf_ipv4_socket_to_policies_map_A_lookup_elem(&cookie);
+        } else {
+            v4Policy = bpf_ipv4_socket_to_policies_map_B_lookup_elem(&cookie);
+        }
+
+        // How to use bitmask here to compare params efficiently?
+        // TODO: add BPF_PROG_TYPE_SK_SKB prog type to Loader?
+
+        void* data = (void*)(long)skb->data;
+        const void* data_end = (void*)(long)skb->data_end;
+        const struct iphdr* const iph = data;
+
+        // Must have ipv4 header
+        if (data + sizeof(*iph) > data_end) return TC_ACT_PIPE;
+
+        // IP version must be 4
+        if (iph->version != 4) return TC_ACT_PIPE;
+
+        // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
+        if (iph->ihl != 5) return TC_ACT_PIPE;
+
+        if (iph->protocol != IPPROTO_UDP) return TC_ACT_PIPE;
+
+        struct udphdr *udp;
+        udp = data + sizeof(struct iphdr); //sizeof(struct ethhdr)
+
+        if ((void*)(udp + 1) > data_end) return TC_ACT_PIPE;
+
+        // Source/destination port in udphdr are stored in be16, need to convert to le16.
+        // This can be done via ntohs or htons. Is there a more preferred way?
+        // Cached policy was found.
+        if (v4Policy && iph->saddr == v4Policy->srcIp.s_addr &&
+                    iph->daddr == v4Policy->dstIp.s_addr &&
+                    ntohs(udp->source) == v4Policy->srcPort &&
+                    ntohs(udp->dest) == v4Policy->dstPort &&
+                    iph->protocol == v4Policy->proto) {
+            // set dscpVal in packet. Least sig 2 bits of TOS
+            // reference ipv4_change_dsfield()
+
+            // TODO: fix checksum...
+            int ecn = iph->tos & 3;
+            uint8_t newDscpVal = (v4Policy->dscpVal << 2) + ecn;
+            int oldDscpVal = iph->tos >> 2;
+            bpf_l3_csum_replace(skb, 1, oldDscpVal, newDscpVal, sizeof(uint8_t));
+            bpf_skb_store_bytes(skb, 1, &newDscpVal, sizeof(uint8_t), 0);
+            return TC_ACT_PIPE;
+        }
+
+        // linear scan ipv4_dscp_policies_map, stored socket params do not match actual
+        int bestScore = -1;
+        uint32_t bestMatch = 0;
+
+        for (register uint64_t i = 0; i < MAX_POLICIES; i++) {
+            int score = 0;
+            uint8_t tempMask = 0;
+            // Using a uint62 in for loop prevents infinite loop during BPF load,
+            // but the key is uint32, so convert back.
+            uint32_t key = i;
+            Ipv4Policy* policy = bpf_ipv4_dscp_policies_map_lookup_elem(&key);
+
+            // if mask is 0 continue, key does not have corresponding policy value
+            if (policy && policy->mask != 0) {
+                if ((policy->mask & SRC_IP_MASK) == SRC_IP_MASK &&
+                        iph->saddr == policy->srcIp.s6_addr32[3]) {
+                    score++;
+                    tempMask |= SRC_IP_MASK;
+                }
+                if ((policy->mask & DST_IP_MASK) == DST_IP_MASK &&
+                        iph->daddr == policy->dstIp.s6_addr32[3]) {
+                    score++;
+                    tempMask |= DST_IP_MASK;
+                }
+                if ((policy->mask & SRC_PORT_MASK) == SRC_PORT_MASK &&
+                        ntohs(udp->source) == htons(policy->srcPort)) {
+                    score++;
+                    tempMask |= SRC_PORT_MASK;
+                }
+                if ((policy->mask & DST_PORT_MASK) == DST_PORT_MASK &&
+                        ntohs(udp->dest) >= htons(policy->dstPortStart) &&
+                        ntohs(udp->dest) <= htons(policy->dstPortEnd)) {
+                    score++;
+                    tempMask |= DST_PORT_MASK;
+                }
+                if ((policy->mask & PROTO_MASK) == PROTO_MASK &&
+                        iph->protocol == policy->proto) {
+                    score++;
+                    tempMask |= PROTO_MASK;
+                }
+
+                if (score > bestScore && tempMask == policy->mask) {
+                    bestMatch = i;
+                    bestScore = score;
+                }
+            }
+        }
+
+        uint8_t newDscpVal = 0; // Can 0 be used as default forwarding value?
+        uint8_t curDscp = iph->tos & 252;
+        if (bestScore > 0) {
+            Ipv4Policy* policy = bpf_ipv4_dscp_policies_map_lookup_elem(&bestMatch);
+            if (policy) {
+                // TODO: if DSCP value is already set ignore?
+                // TODO: update checksum, for testing increment counter...
+                int ecn = iph->tos & 3;
+                newDscpVal = (policy->dscpVal << 2) + ecn;
+            }
+        }
+
+        Ipv4RuleEntry value = {
+            .srcIp.s_addr = iph->saddr,
+            .dstIp.s_addr = iph->daddr,
+            .srcPort = udp->source,
+            .dstPort = udp->dest,
+            .proto = iph->protocol,
+            .dscpVal = newDscpVal,
+        };
+
+        if (!cookie)
+            return TC_ACT_PIPE;
+
+        // Update map
+        if (*selectedMap == MAP_A) {
+            bpf_ipv4_socket_to_policies_map_A_update_elem(&cookie, &value, BPF_ANY);
+        } else {
+            bpf_ipv4_socket_to_policies_map_B_update_elem(&cookie, &value, BPF_ANY);
+        }
+
+        // Need to store bytes after updating map or program will not load.
+        if (newDscpVal != curDscp) {
+            // 1 is the offset (Version/Header length)
+            int oldDscpVal = iph->tos >> 2;
+            bpf_l3_csum_replace(skb, 1, oldDscpVal, newDscpVal, sizeof(uint8_t));
+            bpf_skb_store_bytes(skb, 1, &newDscpVal, sizeof(uint8_t), 0);
+        }
+
+    } else if (skb->protocol == htons(ETH_P_IPV6)) { //maybe bpf_htons()
+        Ipv6RuleEntry* v6Policy;
+        if (*selectedMap == MAP_A) {
+            v6Policy = bpf_ipv6_socket_to_policies_map_A_lookup_elem(&cookie);
+        } else {
+            v6Policy = bpf_ipv6_socket_to_policies_map_B_lookup_elem(&cookie);
+        }
+
+        if (!v6Policy)
+            return TC_ACT_PIPE;
+
+        // TODO: Add code to process IPv6 packet.
+    }
+
+    // Always return TC_ACT_PIPE
+    return TC_ACT_PIPE;
+}
+
+LICENSE("Apache 2.0");
+CRITICAL("Connectivity");
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
new file mode 100644
index 0000000..f0af8b4
--- /dev/null
+++ b/bpf_progs/netd.c
@@ -0,0 +1,393 @@
+/*
+ * Copyright (C) 2018 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 <bpf_helpers.h>
+#include <linux/bpf.h>
+#include <linux/if.h>
+#include <linux/if_ether.h>
+#include <linux/if_packet.h>
+#include <linux/in.h>
+#include <linux/in6.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/pkt_cls.h>
+#include <linux/tcp.h>
+#include <netdutils/UidConstants.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include "bpf_net_helpers.h"
+#include "bpf_shared.h"
+
+// This is defined for cgroup bpf filter only.
+#define BPF_DROP_UNLESS_DNS 2
+#define BPF_PASS 1
+#define BPF_DROP 0
+
+// This is used for xt_bpf program only.
+#define BPF_NOMATCH 0
+#define BPF_MATCH 1
+
+#define BPF_EGRESS 0
+#define BPF_INGRESS 1
+
+#define IP_PROTO_OFF offsetof(struct iphdr, protocol)
+#define IPV6_PROTO_OFF offsetof(struct ipv6hdr, nexthdr)
+#define IPPROTO_IHL_OFF 0
+#define TCP_FLAG_OFF 13
+#define RST_OFFSET 2
+
+DEFINE_BPF_MAP_GRW(cookie_tag_map, HASH, uint64_t, UidTagValue, COOKIE_UID_MAP_SIZE,
+                   AID_NET_BW_ACCT)
+DEFINE_BPF_MAP_GRW(uid_counterset_map, HASH, uint32_t, uint8_t, UID_COUNTERSET_MAP_SIZE,
+                   AID_NET_BW_ACCT)
+DEFINE_BPF_MAP_GRW(app_uid_stats_map, HASH, uint32_t, StatsValue, APP_STATS_MAP_SIZE,
+                   AID_NET_BW_ACCT)
+DEFINE_BPF_MAP_GRW(stats_map_A, HASH, StatsKey, StatsValue, STATS_MAP_SIZE, AID_NET_BW_ACCT)
+DEFINE_BPF_MAP_GRW(stats_map_B, HASH, StatsKey, StatsValue, STATS_MAP_SIZE, AID_NET_BW_ACCT)
+DEFINE_BPF_MAP_GRW(iface_stats_map, HASH, uint32_t, StatsValue, IFACE_STATS_MAP_SIZE,
+                   AID_NET_BW_ACCT)
+DEFINE_BPF_MAP_GRW(configuration_map, HASH, uint32_t, uint8_t, CONFIGURATION_MAP_SIZE,
+                   AID_NET_BW_ACCT)
+DEFINE_BPF_MAP_GRW(uid_owner_map, HASH, uint32_t, UidOwnerValue, UID_OWNER_MAP_SIZE,
+                   AID_NET_BW_ACCT)
+DEFINE_BPF_MAP_GRW(uid_permission_map, HASH, uint32_t, uint8_t, UID_OWNER_MAP_SIZE, AID_NET_BW_ACCT)
+
+/* never actually used from ebpf */
+DEFINE_BPF_MAP_GRW(iface_index_name_map, HASH, uint32_t, IfaceValue, IFACE_INDEX_NAME_MAP_SIZE,
+                   AID_NET_BW_ACCT)
+
+static __always_inline int is_system_uid(uint32_t uid) {
+    return (uid <= MAX_SYSTEM_UID) && (uid >= MIN_SYSTEM_UID);
+}
+
+/*
+ * Note: this blindly assumes an MTU of 1500, and that packets > MTU are always TCP,
+ * and that TCP is using the Linux default settings with TCP timestamp option enabled
+ * which uses 12 TCP option bytes per frame.
+ *
+ * These are not unreasonable assumptions:
+ *
+ * The internet does not really support MTUs greater than 1500, so most TCP traffic will
+ * be at that MTU, or slightly below it (worst case our upwards adjustment is too small).
+ *
+ * The chance our traffic isn't IP at all is basically zero, so the IP overhead correction
+ * is bound to be needed.
+ *
+ * Furthermore, the likelyhood that we're having to deal with GSO (ie. > MTU) packets that
+ * are not IP/TCP is pretty small (few other things are supported by Linux) and worse case
+ * our extra overhead will be slightly off, but probably still better than assuming none.
+ *
+ * Most servers are also Linux and thus support/default to using TCP timestamp option
+ * (and indeed TCP timestamp option comes from RFC 1323 titled "TCP Extensions for High
+ * Performance" which also defined TCP window scaling and are thus absolutely ancient...).
+ *
+ * All together this should be more correct than if we simply ignored GSO frames
+ * (ie. counted them as single packets with no extra overhead)
+ *
+ * Especially since the number of packets is important for any future clat offload correction.
+ * (which adjusts upward by 20 bytes per packet to account for ipv4 -> ipv6 header conversion)
+ */
+#define DEFINE_UPDATE_STATS(the_stats_map, TypeOfKey)                                          \
+    static __always_inline inline void update_##the_stats_map(struct __sk_buff* skb,           \
+                                                              int direction, TypeOfKey* key) { \
+        StatsValue* value = bpf_##the_stats_map##_lookup_elem(key);                            \
+        if (!value) {                                                                          \
+            StatsValue newValue = {};                                                          \
+            bpf_##the_stats_map##_update_elem(key, &newValue, BPF_NOEXIST);                    \
+            value = bpf_##the_stats_map##_lookup_elem(key);                                    \
+        }                                                                                      \
+        if (value) {                                                                           \
+            const int mtu = 1500;                                                              \
+            uint64_t packets = 1;                                                              \
+            uint64_t bytes = skb->len;                                                         \
+            if (bytes > mtu) {                                                                 \
+                bool is_ipv6 = (skb->protocol == htons(ETH_P_IPV6));                           \
+                int ip_overhead = (is_ipv6 ? sizeof(struct ipv6hdr) : sizeof(struct iphdr));   \
+                int tcp_overhead = ip_overhead + sizeof(struct tcphdr) + 12;                   \
+                int mss = mtu - tcp_overhead;                                                  \
+                uint64_t payload = bytes - tcp_overhead;                                       \
+                packets = (payload + mss - 1) / mss;                                           \
+                bytes = tcp_overhead * packets + payload;                                      \
+            }                                                                                  \
+            if (direction == BPF_EGRESS) {                                                     \
+                __sync_fetch_and_add(&value->txPackets, packets);                              \
+                __sync_fetch_and_add(&value->txBytes, bytes);                                  \
+            } else if (direction == BPF_INGRESS) {                                             \
+                __sync_fetch_and_add(&value->rxPackets, packets);                              \
+                __sync_fetch_and_add(&value->rxBytes, bytes);                                  \
+            }                                                                                  \
+        }                                                                                      \
+    }
+
+DEFINE_UPDATE_STATS(app_uid_stats_map, uint32_t)
+DEFINE_UPDATE_STATS(iface_stats_map, uint32_t)
+DEFINE_UPDATE_STATS(stats_map_A, StatsKey)
+DEFINE_UPDATE_STATS(stats_map_B, StatsKey)
+
+static inline bool skip_owner_match(struct __sk_buff* skb) {
+    int offset = -1;
+    int ret = 0;
+    if (skb->protocol == htons(ETH_P_IP)) {
+        offset = IP_PROTO_OFF;
+        uint8_t proto, ihl;
+        uint8_t flag;
+        ret = bpf_skb_load_bytes(skb, offset, &proto, 1);
+        if (!ret) {
+            if (proto == IPPROTO_ESP) {
+                return true;
+            } else if (proto == IPPROTO_TCP) {
+                ret = bpf_skb_load_bytes(skb, IPPROTO_IHL_OFF, &ihl, 1);
+                ihl = ihl & 0x0F;
+                ret = bpf_skb_load_bytes(skb, ihl * 4 + TCP_FLAG_OFF, &flag, 1);
+                if (ret == 0 && (flag >> RST_OFFSET & 1)) {
+                    return true;
+                }
+            }
+        }
+    } else if (skb->protocol == htons(ETH_P_IPV6)) {
+        offset = IPV6_PROTO_OFF;
+        uint8_t proto;
+        ret = bpf_skb_load_bytes(skb, offset, &proto, 1);
+        if (!ret) {
+            if (proto == IPPROTO_ESP) {
+                return true;
+            } else if (proto == IPPROTO_TCP) {
+                uint8_t flag;
+                ret = bpf_skb_load_bytes(skb, sizeof(struct ipv6hdr) + TCP_FLAG_OFF, &flag, 1);
+                if (ret == 0 && (flag >> RST_OFFSET & 1)) {
+                    return true;
+                }
+            }
+        }
+    }
+    return false;
+}
+
+static __always_inline BpfConfig getConfig(uint32_t configKey) {
+    uint32_t mapSettingKey = configKey;
+    BpfConfig* config = bpf_configuration_map_lookup_elem(&mapSettingKey);
+    if (!config) {
+        // Couldn't read configuration entry. Assume everything is disabled.
+        return DEFAULT_CONFIG;
+    }
+    return *config;
+}
+
+static inline int bpf_owner_match(struct __sk_buff* skb, uint32_t uid, int direction) {
+    if (skip_owner_match(skb)) return BPF_PASS;
+
+    if (is_system_uid(uid)) return BPF_PASS;
+
+    BpfConfig enabledRules = getConfig(UID_RULES_CONFIGURATION_KEY);
+
+    UidOwnerValue* uidEntry = bpf_uid_owner_map_lookup_elem(&uid);
+    uint8_t uidRules = uidEntry ? uidEntry->rule : 0;
+    uint32_t allowed_iif = uidEntry ? uidEntry->iif : 0;
+
+    if (enabledRules) {
+        if ((enabledRules & DOZABLE_MATCH) && !(uidRules & DOZABLE_MATCH)) {
+            return BPF_DROP;
+        }
+        if ((enabledRules & STANDBY_MATCH) && (uidRules & STANDBY_MATCH)) {
+            return BPF_DROP;
+        }
+        if ((enabledRules & POWERSAVE_MATCH) && !(uidRules & POWERSAVE_MATCH)) {
+            return BPF_DROP;
+        }
+        if ((enabledRules & RESTRICTED_MATCH) && !(uidRules & RESTRICTED_MATCH)) {
+            return BPF_DROP;
+        }
+    }
+    if (direction == BPF_INGRESS && (uidRules & IIF_MATCH)) {
+        // Drops packets not coming from lo nor the allowlisted interface
+        if (allowed_iif && skb->ifindex != 1 && skb->ifindex != allowed_iif) {
+            return BPF_DROP_UNLESS_DNS;
+        }
+    }
+    return BPF_PASS;
+}
+
+static __always_inline inline void update_stats_with_config(struct __sk_buff* skb, int direction,
+                                                            StatsKey* key, uint8_t selectedMap) {
+    if (selectedMap == SELECT_MAP_A) {
+        update_stats_map_A(skb, direction, key);
+    } else if (selectedMap == SELECT_MAP_B) {
+        update_stats_map_B(skb, direction, key);
+    }
+}
+
+static __always_inline inline int bpf_traffic_account(struct __sk_buff* skb, int direction) {
+    uint32_t sock_uid = bpf_get_socket_uid(skb);
+    uint64_t cookie = bpf_get_socket_cookie(skb);
+    UidTagValue* utag = bpf_cookie_tag_map_lookup_elem(&cookie);
+    uint32_t uid, tag;
+    if (utag) {
+        uid = utag->uid;
+        tag = utag->tag;
+    } else {
+        uid = sock_uid;
+        tag = 0;
+    }
+
+    // Always allow and never count clat traffic. Only the IPv4 traffic on the stacked
+    // interface is accounted for and subject to usage restrictions.
+    // TODO: remove sock_uid check once Nat464Xlat javaland adds the socket tag AID_CLAT for clat.
+    if (sock_uid == AID_CLAT || uid == AID_CLAT) {
+        return BPF_PASS;
+    }
+
+    int match = bpf_owner_match(skb, sock_uid, direction);
+    if ((direction == BPF_EGRESS) && (match == BPF_DROP)) {
+        // If an outbound packet is going to be dropped, we do not count that
+        // traffic.
+        return match;
+    }
+
+// Workaround for secureVPN with VpnIsolation enabled, refer to b/159994981 for details.
+// Keep TAG_SYSTEM_DNS in sync with DnsResolver/include/netd_resolv/resolv.h
+// and TrafficStatsConstants.java
+#define TAG_SYSTEM_DNS 0xFFFFFF82
+    if (tag == TAG_SYSTEM_DNS && uid == AID_DNS) {
+        uid = sock_uid;
+        if (match == BPF_DROP_UNLESS_DNS) match = BPF_PASS;
+    } else {
+        if (match == BPF_DROP_UNLESS_DNS) match = BPF_DROP;
+    }
+
+    StatsKey key = {.uid = uid, .tag = tag, .counterSet = 0, .ifaceIndex = skb->ifindex};
+
+    uint8_t* counterSet = bpf_uid_counterset_map_lookup_elem(&uid);
+    if (counterSet) key.counterSet = (uint32_t)*counterSet;
+
+    uint32_t mapSettingKey = CURRENT_STATS_MAP_CONFIGURATION_KEY;
+    uint8_t* selectedMap = bpf_configuration_map_lookup_elem(&mapSettingKey);
+
+    // Use asm("%0 &= 1" : "+r"(match)) before return match,
+    // to help kernel's bpf verifier, so that it can be 100% certain
+    // that the returned value is always BPF_NOMATCH(0) or BPF_MATCH(1).
+    if (!selectedMap) {
+        asm("%0 &= 1" : "+r"(match));
+        return match;
+    }
+
+    if (key.tag) {
+        update_stats_with_config(skb, direction, &key, *selectedMap);
+        key.tag = 0;
+    }
+
+    update_stats_with_config(skb, direction, &key, *selectedMap);
+    update_app_uid_stats_map(skb, direction, &uid);
+    asm("%0 &= 1" : "+r"(match));
+    return match;
+}
+
+DEFINE_BPF_PROG("cgroupskb/ingress/stats", AID_ROOT, AID_ROOT, bpf_cgroup_ingress)
+(struct __sk_buff* skb) {
+    return bpf_traffic_account(skb, BPF_INGRESS);
+}
+
+DEFINE_BPF_PROG("cgroupskb/egress/stats", AID_ROOT, AID_ROOT, bpf_cgroup_egress)
+(struct __sk_buff* skb) {
+    return bpf_traffic_account(skb, BPF_EGRESS);
+}
+
+DEFINE_BPF_PROG("skfilter/egress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_egress_prog)
+(struct __sk_buff* skb) {
+    // Clat daemon does not generate new traffic, all its traffic is accounted for already
+    // on the v4-* interfaces (except for the 20 (or 28) extra bytes of IPv6 vs IPv4 overhead,
+    // but that can be corrected for later when merging v4-foo stats into interface foo's).
+    // TODO: remove sock_uid check once Nat464Xlat javaland adds the socket tag AID_CLAT for clat.
+    uint32_t sock_uid = bpf_get_socket_uid(skb);
+    if (sock_uid == AID_CLAT) return BPF_NOMATCH;
+    if (sock_uid == AID_SYSTEM) {
+        uint64_t cookie = bpf_get_socket_cookie(skb);
+        UidTagValue* utag = bpf_cookie_tag_map_lookup_elem(&cookie);
+        if (utag && utag->uid == AID_CLAT) return BPF_NOMATCH;
+    }
+
+    uint32_t key = skb->ifindex;
+    update_iface_stats_map(skb, BPF_EGRESS, &key);
+    return BPF_MATCH;
+}
+
+DEFINE_BPF_PROG("skfilter/ingress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_ingress_prog)
+(struct __sk_buff* skb) {
+    // Clat daemon traffic is not accounted by virtue of iptables raw prerouting drop rule
+    // (in clat_raw_PREROUTING chain), which triggers before this (in bw_raw_PREROUTING chain).
+    // It will be accounted for on the v4-* clat interface instead.
+    // Keep that in mind when moving this out of iptables xt_bpf and into tc ingress (or xdp).
+
+    uint32_t key = skb->ifindex;
+    update_iface_stats_map(skb, BPF_INGRESS, &key);
+    return BPF_MATCH;
+}
+
+DEFINE_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN, tc_bpf_ingress_account_prog)
+(struct __sk_buff* skb) {
+    // Account for ingress traffic before tc drops it.
+    uint32_t key = skb->ifindex;
+    update_iface_stats_map(skb, BPF_INGRESS, &key);
+    return TC_ACT_UNSPEC;
+}
+
+DEFINE_BPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog)
+(struct __sk_buff* skb) {
+    uint32_t sock_uid = bpf_get_socket_uid(skb);
+    if (is_system_uid(sock_uid)) return BPF_MATCH;
+
+    // 65534 is the overflow 'nobody' uid, usually this being returned means
+    // that skb->sk is NULL during RX (early decap socket lookup failure),
+    // which commonly happens for incoming packets to an unconnected udp socket.
+    // Additionally bpf_get_socket_cookie() returns 0 if skb->sk is NULL
+    if ((sock_uid == 65534) && !bpf_get_socket_cookie(skb) && is_received_skb(skb))
+        return BPF_MATCH;
+
+    UidOwnerValue* allowlistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
+    if (allowlistMatch) return allowlistMatch->rule & HAPPY_BOX_MATCH ? BPF_MATCH : BPF_NOMATCH;
+    return BPF_NOMATCH;
+}
+
+DEFINE_BPF_PROG("skfilter/denylist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_denylist_prog)
+(struct __sk_buff* skb) {
+    uint32_t sock_uid = bpf_get_socket_uid(skb);
+    UidOwnerValue* denylistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
+    if (denylistMatch) return denylistMatch->rule & PENALTY_BOX_MATCH ? BPF_MATCH : BPF_NOMATCH;
+    return BPF_NOMATCH;
+}
+
+DEFINE_BPF_PROG_KVER("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create,
+                     KVER(4, 14, 0))
+(struct bpf_sock* sk) {
+    uint64_t gid_uid = bpf_get_current_uid_gid();
+    /*
+     * A given app is guaranteed to have the same app ID in all the profiles in
+     * which it is installed, and install permission is granted to app for all
+     * user at install time so we only check the appId part of a request uid at
+     * run time. See UserHandle#isSameApp for detail.
+     */
+    uint32_t appId = (gid_uid & 0xffffffff) % PER_USER_RANGE;
+    uint8_t* permissions = bpf_uid_permission_map_lookup_elem(&appId);
+    if (!permissions) {
+        // UID not in map. Default to just INTERNET permission.
+        return 1;
+    }
+
+    // A return value of 1 means allow, everything else means deny.
+    return (*permissions & BPF_PERMISSION_INTERNET) == BPF_PERMISSION_INTERNET;
+}
+
+LICENSE("Apache 2.0");
+CRITICAL("netd");
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index 79bb128..abcfbeb 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -19,11 +19,22 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+java_defaults {
+    name: "enable-framework-connectivity-t-targets",
+    enabled: true,
+}
+// The above defaults can be used to disable framework-connectivity t
+// targets while minimizing merge conflicts in the build rules.
+
+
 java_sdk_library {
     name: "framework-connectivity-tiramisu",
     sdk_version: "module_current",
     min_sdk_version: "Tiramisu",
-    defaults: ["framework-module-defaults"],
+    defaults: [
+        "framework-module-defaults",
+        "enable-framework-connectivity-t-targets",
+    ],
     srcs: [
         ":framework-connectivity-tiramisu-updatable-sources",
     ],
diff --git a/framework/aidl-export/android/net/DscpPolicy.aidl b/framework/aidl-export/android/net/DscpPolicy.aidl
new file mode 100644
index 0000000..8da42ca
--- /dev/null
+++ b/framework/aidl-export/android/net/DscpPolicy.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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 android.net;
+
+@JavaOnlyStableParcelable parcelable DscpPolicy;
diff --git a/framework/api/current.txt b/framework/api/current.txt
index 827da6d..a373b71 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -294,7 +294,7 @@
     ctor public NetworkCapabilities(android.net.NetworkCapabilities);
     method public int describeContents();
     method @NonNull public int[] getCapabilities();
-    method @NonNull public int[] getEnterpriseCapabilitySubLevels();
+    method @NonNull public int[] getEnterpriseIds();
     method public int getLinkDownstreamBandwidthKbps();
     method public int getLinkUpstreamBandwidthKbps();
     method @Nullable public android.net.NetworkSpecifier getNetworkSpecifier();
@@ -302,6 +302,7 @@
     method public int getSignalStrength();
     method @Nullable public android.net.TransportInfo getTransportInfo();
     method public boolean hasCapability(int);
+    method public boolean hasEnterpriseId(int);
     method public boolean hasTransport(int);
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkCapabilities> CREATOR;
@@ -325,6 +326,8 @@
     field public static final int NET_CAPABILITY_NOT_ROAMING = 18; // 0x12
     field public static final int NET_CAPABILITY_NOT_SUSPENDED = 21; // 0x15
     field public static final int NET_CAPABILITY_NOT_VPN = 15; // 0xf
+    field public static final int NET_CAPABILITY_PRIORITIZE_BANDWIDTH = 35; // 0x23
+    field public static final int NET_CAPABILITY_PRIORITIZE_LATENCY = 34; // 0x22
     field public static final int NET_CAPABILITY_RCS = 8; // 0x8
     field public static final int NET_CAPABILITY_SUPL = 1; // 0x1
     field public static final int NET_CAPABILITY_TEMPORARILY_NOT_METERED = 25; // 0x19
@@ -332,6 +335,11 @@
     field public static final int NET_CAPABILITY_VALIDATED = 16; // 0x10
     field public static final int NET_CAPABILITY_WIFI_P2P = 6; // 0x6
     field public static final int NET_CAPABILITY_XCAP = 9; // 0x9
+    field public static final int NET_ENTERPRISE_ID_1 = 1; // 0x1
+    field public static final int NET_ENTERPRISE_ID_2 = 2; // 0x2
+    field public static final int NET_ENTERPRISE_ID_3 = 3; // 0x3
+    field public static final int NET_ENTERPRISE_ID_4 = 4; // 0x4
+    field public static final int NET_ENTERPRISE_ID_5 = 5; // 0x5
     field public static final int SIGNAL_STRENGTH_UNSPECIFIED = -2147483648; // 0x80000000
     field public static final int TRANSPORT_BLUETOOTH = 2; // 0x2
     field public static final int TRANSPORT_CELLULAR = 0; // 0x0
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index 415b3e5..8da421d 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -127,17 +127,13 @@
 
   public static final class NetworkAgentConfig.Builder {
     method @NonNull public android.net.NetworkAgentConfig.Builder setBypassableVpn(boolean);
+    method @NonNull public android.net.NetworkAgentConfig.Builder setExcludeLocalRoutesVpn(boolean);
     method @NonNull public android.net.NetworkAgentConfig.Builder setSubscriberId(@Nullable String);
   }
 
   public final class NetworkCapabilities implements android.os.Parcelable {
     method @Nullable public java.util.Set<android.util.Range<java.lang.Integer>> getUids();
     method public boolean hasForbiddenCapability(int);
-    field public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1 = 1; // 0x1
-    field public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2 = 2; // 0x2
-    field public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3 = 3; // 0x3
-    field public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4 = 4; // 0x4
-    field public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5 = 5; // 0x5
     field public static final long REDACT_ALL = -1L; // 0xffffffffffffffffL
     field public static final long REDACT_FOR_ACCESS_FINE_LOCATION = 1L; // 0x1L
     field public static final long REDACT_FOR_LOCAL_MAC_ADDRESS = 2L; // 0x2L
@@ -151,7 +147,9 @@
   }
 
   public class NetworkRequest implements android.os.Parcelable {
+    method @NonNull public int[] getEnterpriseIds();
     method @NonNull public int[] getForbiddenCapabilities();
+    method public boolean hasEnterpriseId(int);
     method public boolean hasForbiddenCapability(int);
   }
 
@@ -163,7 +161,10 @@
 
   public final class ProfileNetworkPreference implements android.os.Parcelable {
     method public int describeContents();
+    method @NonNull public java.util.List<java.lang.Integer> getExcludedUids();
+    method @NonNull public java.util.List<java.lang.Integer> getIncludedUids();
     method public int getPreference();
+    method public int getPreferenceEnterpriseId();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.net.ProfileNetworkPreference> CREATOR;
   }
@@ -171,7 +172,10 @@
   public static final class ProfileNetworkPreference.Builder {
     ctor public ProfileNetworkPreference.Builder();
     method @NonNull public android.net.ProfileNetworkPreference build();
+    method @NonNull public android.net.ProfileNetworkPreference.Builder setExcludedUids(@Nullable java.util.List<java.lang.Integer>);
+    method @NonNull public android.net.ProfileNetworkPreference.Builder setIncludedUids(@Nullable java.util.List<java.lang.Integer>);
     method @NonNull public android.net.ProfileNetworkPreference.Builder setPreference(int);
+    method @NonNull public android.net.ProfileNetworkPreference.Builder setPreferenceEnterpriseId(int);
   }
 
   public final class TestNetworkInterface implements android.os.Parcelable {
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index 8d084e6..d420958 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -93,6 +93,35 @@
     method @Deprecated public void onUpstreamChanged(@Nullable android.net.Network);
   }
 
+  public final class DscpPolicy implements android.os.Parcelable {
+    method @Nullable public java.net.InetAddress getDestinationAddress();
+    method @Nullable public android.util.Range<java.lang.Integer> getDestinationPortRange();
+    method public int getDscpValue();
+    method public int getPolicyId();
+    method public int getProtocol();
+    method @Nullable public java.net.InetAddress getSourceAddress();
+    method public int getSourcePort();
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.DscpPolicy> CREATOR;
+    field public static final int PROTOCOL_ANY = -1; // 0xffffffff
+    field public static final int SOURCE_PORT_ANY = -1; // 0xffffffff
+    field public static final int STATUS_DELETED = 4; // 0x4
+    field public static final int STATUS_INSUFFICIENT_PROCESSING_RESOURCES = 3; // 0x3
+    field public static final int STATUS_POLICY_NOT_FOUND = 5; // 0x5
+    field public static final int STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED = 2; // 0x2
+    field public static final int STATUS_REQUEST_DECLINED = 1; // 0x1
+    field public static final int STATUS_SUCCESS = 0; // 0x0
+  }
+
+  public static final class DscpPolicy.Builder {
+    ctor public DscpPolicy.Builder(int, int);
+    method @NonNull public android.net.DscpPolicy build();
+    method @NonNull public android.net.DscpPolicy.Builder setDestinationAddress(@NonNull java.net.InetAddress);
+    method @NonNull public android.net.DscpPolicy.Builder setDestinationPortRange(@NonNull android.util.Range<java.lang.Integer>);
+    method @NonNull public android.net.DscpPolicy.Builder setProtocol(int);
+    method @NonNull public android.net.DscpPolicy.Builder setSourceAddress(@NonNull java.net.InetAddress);
+    method @NonNull public android.net.DscpPolicy.Builder setSourcePort(int);
+  }
+
   public final class InvalidPacketException extends java.lang.Exception {
     ctor public InvalidPacketException(int);
     method public int getError();
@@ -217,6 +246,7 @@
     method public void onAddKeepalivePacketFilter(int, @NonNull android.net.KeepalivePacketData);
     method public void onAutomaticReconnectDisabled();
     method public void onBandwidthUpdateRequested();
+    method public void onDscpPolicyStatusUpdated(int, int);
     method public void onNetworkCreated();
     method public void onNetworkDestroyed();
     method public void onNetworkUnwanted();
@@ -229,6 +259,7 @@
     method public void onStopSocketKeepalive(int);
     method public void onValidationStatus(int, @Nullable android.net.Uri);
     method @NonNull public android.net.Network register();
+    method public void sendAddDscpPolicy(@NonNull android.net.DscpPolicy);
     method public final void sendLinkProperties(@NonNull android.net.LinkProperties);
     method public final void sendNetworkCapabilities(@NonNull android.net.NetworkCapabilities);
     method public final void sendNetworkScore(@NonNull android.net.NetworkScore);
@@ -236,6 +267,8 @@
     method public final void sendQosCallbackError(int, int);
     method public final void sendQosSessionAvailable(int, int, @NonNull android.net.QosSessionAttributes);
     method public final void sendQosSessionLost(int, int, int);
+    method public void sendRemoveAllDscpPolicies();
+    method public void sendRemoveDscpPolicy(int);
     method public final void sendSocketKeepaliveEvent(int, int);
     method @Deprecated public void setLegacySubtype(int, @NonNull String);
     method public void setLingerDuration(@NonNull java.time.Duration);
@@ -294,11 +327,11 @@
     ctor public NetworkCapabilities.Builder();
     ctor public NetworkCapabilities.Builder(@NonNull android.net.NetworkCapabilities);
     method @NonNull public android.net.NetworkCapabilities.Builder addCapability(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder addEnterpriseCapabilitySubLevel(int);
+    method @NonNull public android.net.NetworkCapabilities.Builder addEnterpriseId(int);
     method @NonNull public android.net.NetworkCapabilities.Builder addTransportType(int);
     method @NonNull public android.net.NetworkCapabilities build();
     method @NonNull public android.net.NetworkCapabilities.Builder removeCapability(int);
-    method @NonNull public android.net.NetworkCapabilities.Builder removeEnterpriseCapabilitySubLevel(int);
+    method @NonNull public android.net.NetworkCapabilities.Builder removeEnterpriseId(int);
     method @NonNull public android.net.NetworkCapabilities.Builder removeTransportType(int);
     method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setAdministratorUids(@NonNull int[]);
     method @NonNull public android.net.NetworkCapabilities.Builder setLinkDownstreamBandwidthKbps(int);
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 7d65f8c..7593482 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -16,6 +16,7 @@
 package android.net;
 
 import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
 import static android.net.NetworkRequest.Type.BACKGROUND_REQUEST;
 import static android.net.NetworkRequest.Type.LISTEN;
 import static android.net.NetworkRequest.Type.LISTEN_FOR_BEST;
@@ -5537,6 +5538,9 @@
         ProfileNetworkPreference.Builder preferenceBuilder =
                 new ProfileNetworkPreference.Builder();
         preferenceBuilder.setPreference(preference);
+        if (preference != PROFILE_NETWORK_PREFERENCE_DEFAULT) {
+            preferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        }
         setProfileNetworkPreferences(profile,
                 List.of(preferenceBuilder.build()), executor, listener);
     }
@@ -5612,7 +5616,7 @@
      * background data is restricted.
      *
      * @param uid uid of target app
-     * @throws IllegalStateException if update allow list failed.
+     * @throws IllegalStateException if updating allow list failed.
      * @hide
      */
     @SystemApi(client = MODULE_LIBRARIES)
@@ -5634,7 +5638,7 @@
      * Takes precedence over {@link #updateMeteredNetworkAllowList}.
      *
      * @param uid uid of target app
-     * @throws IllegalStateException if update deny list failed.
+     * @throws IllegalStateException if updating deny list failed.
      * @hide
      */
     @SystemApi(client = MODULE_LIBRARIES)
@@ -5656,8 +5660,8 @@
      *
      * @param chain target chain.
      * @param uid uid to allow/deny.
-     * @param allow either add or remove rule.
-     * @throws IllegalStateException if update firewall rule failed.
+     * @param allow whether networking is allowed or denied.
+     * @throws IllegalStateException if updating firewall rule failed.
      * @hide
      */
     @SystemApi(client = MODULE_LIBRARIES)
@@ -5680,7 +5684,7 @@
      *
      * @param chain target chain.
      * @param enable whether the chain should be enabled.
-     * @throws IllegalStateException if set firewall chain failed.
+     * @throws IllegalStateException if enabling or disabling the firewall chain failed.
      * @hide
      */
     @SystemApi(client = MODULE_LIBRARIES)
@@ -5702,7 +5706,7 @@
      *
      * @param chain target chain to replace.
      * @param uids The list of UIDs to be placed into chain.
-     * @throws IllegalStateException if replace firewall chain failed.
+     * @throws IllegalStateException if replacing the firewall chain failed.
      * @throws IllegalArgumentException if {@code chain} is not a valid chain.
      * @hide
      */
@@ -5727,7 +5731,7 @@
      * NetworkStatsFactory which is platform code but will be moved into connectivity (tethering)
      * mainline module.
      *
-     * @throws IllegalStateException if swap active stats map failed.
+     * @throws IllegalStateException if swapping active stats map failed.
      * @hide
      */
     @SystemApi(client = MODULE_LIBRARIES)
diff --git a/framework/src/android/net/DscpPolicy.java b/framework/src/android/net/DscpPolicy.java
new file mode 100644
index 0000000..cda8205
--- /dev/null
+++ b/framework/src/android/net/DscpPolicy.java
@@ -0,0 +1,397 @@
+/*
+ * 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 android.net;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Range;
+
+import com.android.net.module.util.InetAddressUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.Objects;
+
+
+/**
+ * DSCP policy to be set on the requesting NetworkAgent.
+ * @hide
+ */
+@SystemApi
+public final class DscpPolicy implements Parcelable {
+     /**
+     * Indicates that the policy does not specify a protocol.
+     */
+    public static final int PROTOCOL_ANY = -1;
+
+    /**
+     * Indicates that the policy does not specify a port.
+     */
+    public static final int SOURCE_PORT_ANY = -1;
+
+    /**
+     * Policy was successfully added.
+     */
+    public static final int STATUS_SUCCESS = 0;
+
+    /**
+     * Policy was rejected for any reason besides invalid classifier or insufficient resources.
+     */
+    public static final int STATUS_REQUEST_DECLINED = 1;
+
+    /**
+     * Requested policy contained a classifier which is not supported.
+     */
+    public static final int STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED = 2;
+
+    /**
+     * TODO: should this error case be supported?
+     */
+    public static final int STATUS_INSUFFICIENT_PROCESSING_RESOURCES = 3;
+
+    /**
+     * Policy was deleted.
+     */
+    public static final int STATUS_DELETED = 4;
+
+    /**
+     * Policy was not found during deletion.
+     */
+    public static final int STATUS_POLICY_NOT_FOUND = 5;
+
+    /** The unique policy ID. Each requesting network is responsible for maintaining policy IDs
+     * unique within that network. In the case where a policy with an existing ID is created, the
+     * new policy will update the existing policy with the same ID.
+     */
+    private final int mPolicyId;
+
+    /** The QoS DSCP marking to be added to packets matching the policy. */
+    private final int mDscp;
+
+    /** The source IP address. */
+    private final @Nullable InetAddress mSrcAddr;
+
+    /** The destination IP address. */
+    private final @Nullable InetAddress mDstAddr;
+
+    /** The source port. */
+    private final int mSrcPort;
+
+    /** The IP protocol that the policy requires. */
+    private final int mProtocol;
+
+    /** Destination port range. Inclusive range. */
+    private final @Nullable Range<Integer> mDstPortRange;
+
+    /**
+     * Implement the Parcelable interface
+     *
+     * @hide
+     */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** @hide */
+    @IntDef(prefix = "STATUS_", value = {
+        STATUS_SUCCESS,
+        STATUS_REQUEST_DECLINED,
+        STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED,
+        STATUS_INSUFFICIENT_PROCESSING_RESOURCES,
+        STATUS_DELETED
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DscpPolicyStatus {}
+
+    /* package */ DscpPolicy(
+            int policyId,
+            int dscp,
+            @Nullable InetAddress srcAddr,
+            @Nullable InetAddress dstAddr,
+            int srcPort,
+            int protocol,
+            Range<Integer> dstPortRange) {
+        this.mPolicyId = policyId;
+        this.mDscp = dscp;
+        this.mSrcAddr = srcAddr;
+        this.mDstAddr = dstAddr;
+        this.mSrcPort = srcPort;
+        this.mProtocol = protocol;
+        this.mDstPortRange = dstPortRange;
+
+        if (mPolicyId < 1 || mPolicyId > 255) {
+            throw new IllegalArgumentException("Policy ID not in valid range: " + mPolicyId);
+        }
+        if (mDscp < 0 || mDscp > 63) {
+            throw new IllegalArgumentException("DSCP value not in valid range: " + mDscp);
+        }
+        // Since SOURCE_PORT_ANY is the default source port value need to allow it as well.
+        // TODO: Move the default value into this constructor or throw an error from the
+        // instead.
+        if (mSrcPort < -1 || mSrcPort > 65535) {
+            throw new IllegalArgumentException("Source port not in valid range: " + mSrcPort);
+        }
+        if (mDstPortRange != null
+                && (dstPortRange.getLower() < 0 || mDstPortRange.getLower() > 65535)
+                && (mDstPortRange.getUpper() < 0 || mDstPortRange.getUpper() > 65535)) {
+            throw new IllegalArgumentException("Destination port not in valid range");
+        }
+        if (mSrcAddr != null && mDstAddr != null && (mSrcAddr instanceof Inet6Address)
+                != (mDstAddr instanceof Inet6Address)) {
+            throw new IllegalArgumentException("Source/destination address of different family");
+        }
+    }
+
+    /**
+     * The unique policy ID.
+     *
+     * Each requesting network is responsible for maintaining unique
+     * policy IDs. In the case where a policy with an existing ID is created, the new
+     * policy will update the existing policy with the same ID
+     *
+     * @return Policy ID set in Builder.
+     */
+    public int getPolicyId() {
+        return mPolicyId;
+    }
+
+    /**
+     * The QoS DSCP marking to be added to packets matching the policy.
+     *
+     * @return DSCP value set in Builder.
+     */
+    public int getDscpValue() {
+        return mDscp;
+    }
+
+    /**
+     * The source IP address.
+     *
+     * @return Source IP address set in Builder or {@code null} if none was set.
+     */
+    public @Nullable InetAddress getSourceAddress() {
+        return mSrcAddr;
+    }
+
+    /**
+     * The destination IP address.
+     *
+     * @return Destination IP address set in Builder or {@code null} if none was set.
+     */
+    public @Nullable InetAddress getDestinationAddress() {
+        return mDstAddr;
+    }
+
+    /**
+     * The source port.
+     *
+     * @return Source port set in Builder or {@link #SOURCE_PORT_ANY} if no port was set.
+     */
+    public int getSourcePort() {
+        return mSrcPort;
+    }
+
+    /**
+     * The IP protocol that the policy requires.
+     *
+     * @return Protocol set in Builder or {@link #PROTOCOL_ANY} if no protocol was set.
+     *         {@link #PROTOCOL_ANY} indicates that any protocol will be matched.
+     */
+    public int getProtocol() {
+        return mProtocol;
+    }
+
+    /**
+     * Destination port range. Inclusive range.
+     *
+     * @return Range<Integer> set in Builder or {@code null} if none was set.
+     */
+    public @Nullable Range<Integer> getDestinationPortRange() {
+        return mDstPortRange;
+    }
+
+    @Override
+    public String toString() {
+        return "DscpPolicy { "
+                + "policyId = " + mPolicyId + ", "
+                + "dscp = " + mDscp + ", "
+                + "srcAddr = " + mSrcAddr + ", "
+                + "dstAddr = " + mDstAddr + ", "
+                + "srcPort = " + mSrcPort + ", "
+                + "protocol = " + mProtocol + ", "
+                + "dstPortRange = "
+                + (mDstPortRange == null ? "none" : mDstPortRange.toString())
+                + " }";
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) return true;
+        if (!(o instanceof DscpPolicy)) return false;
+        DscpPolicy that = (DscpPolicy) o;
+        return true
+                && mPolicyId == that.mPolicyId
+                && mDscp == that.mDscp
+                && Objects.equals(mSrcAddr, that.mSrcAddr)
+                && Objects.equals(mDstAddr, that.mDstAddr)
+                && mSrcPort == that.mSrcPort
+                && mProtocol == that.mProtocol
+                && Objects.equals(mDstPortRange, that.mDstPortRange);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mPolicyId, mDscp, mSrcAddr.hashCode(),
+                mDstAddr.hashCode(), mSrcPort, mProtocol, mDstPortRange.hashCode());
+    }
+
+    /** @hide */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mPolicyId);
+        dest.writeInt(mDscp);
+        InetAddressUtils.parcelInetAddress(dest, mSrcAddr, flags);
+        InetAddressUtils.parcelInetAddress(dest, mDstAddr, flags);
+        dest.writeInt(mSrcPort);
+        dest.writeInt(mProtocol);
+        dest.writeBoolean(mDstPortRange != null ? true : false);
+        if (mDstPortRange != null) {
+            dest.writeInt(mDstPortRange.getLower());
+            dest.writeInt(mDstPortRange.getUpper());
+        }
+    }
+
+    /** @hide */
+    DscpPolicy(@NonNull Parcel in) {
+        this.mPolicyId = in.readInt();
+        this.mDscp = in.readInt();
+        this.mSrcAddr = InetAddressUtils.unparcelInetAddress(in);
+        this.mDstAddr = InetAddressUtils.unparcelInetAddress(in);
+        this.mSrcPort = in.readInt();
+        this.mProtocol = in.readInt();
+        if (in.readBoolean()) {
+            this.mDstPortRange = new Range<Integer>(in.readInt(), in.readInt());
+        } else {
+            this.mDstPortRange = null;
+        }
+    }
+
+    /** @hide */
+    public @SystemApi static final @NonNull Parcelable.Creator<DscpPolicy> CREATOR =
+            new Parcelable.Creator<DscpPolicy>() {
+                @Override
+                public DscpPolicy[] newArray(int size) {
+                    return new DscpPolicy[size];
+                }
+
+                @Override
+                public DscpPolicy createFromParcel(@NonNull android.os.Parcel in) {
+                    return new DscpPolicy(in);
+                }
+            };
+
+    /**
+     * A builder for {@link DscpPolicy}
+     *
+     */
+    public static final class Builder {
+
+        private final int mPolicyId;
+        private final int mDscp;
+        private @Nullable InetAddress mSrcAddr;
+        private @Nullable InetAddress mDstAddr;
+        private int mSrcPort = SOURCE_PORT_ANY;
+        private int mProtocol = PROTOCOL_ANY;
+        private @Nullable Range<Integer> mDstPortRange;
+
+        private long mBuilderFieldsSet = 0L;
+
+        /**
+         * Creates a new Builder.
+         *
+         * @param policyId The unique policy ID. Each requesting network is responsible for
+         *                 maintaining unique policy IDs. In the case where a policy with an
+         *                 existing ID is created, the new policy will update the existing
+         *                 policy with the same ID
+         * @param dscpValue The DSCP value to set.
+         */
+        public Builder(int policyId, int dscpValue) {
+            mPolicyId = policyId;
+            mDscp = dscpValue;
+        }
+
+        /**
+         * Specifies that this policy matches packets with the specified source IP address.
+         */
+        public @NonNull Builder setSourceAddress(@NonNull InetAddress value) {
+            mSrcAddr = value;
+            return this;
+        }
+
+        /**
+         * Specifies that this policy matches packets with the specified destination IP address.
+         */
+        public @NonNull Builder setDestinationAddress(@NonNull InetAddress value) {
+            mDstAddr = value;
+            return this;
+        }
+
+        /**
+         * Specifies that this policy matches packets with the specified source port.
+         */
+        public @NonNull Builder setSourcePort(int value) {
+            mSrcPort = value;
+            return this;
+        }
+
+        /**
+         * Specifies that this policy matches packets with the specified protocol.
+         */
+        public @NonNull Builder setProtocol(int value) {
+            mProtocol = value;
+            return this;
+        }
+
+        /**
+         * Specifies that this policy matches packets with the specified destination port range.
+         */
+        public @NonNull Builder setDestinationPortRange(@NonNull Range<Integer> range) {
+            mDstPortRange = range;
+            return this;
+        }
+
+        /**
+         * Constructs a DscpPolicy with the specified parameters.
+         */
+        public @NonNull DscpPolicy build() {
+            return new DscpPolicy(
+                    mPolicyId,
+                    mDscp,
+                    mSrcAddr,
+                    mDstAddr,
+                    mSrcPort,
+                    mProtocol,
+                    mDstPortRange);
+        }
+    }
+}
diff --git a/framework/src/android/net/INetworkAgent.aidl b/framework/src/android/net/INetworkAgent.aidl
index d941d4b..fa5175c 100644
--- a/framework/src/android/net/INetworkAgent.aidl
+++ b/framework/src/android/net/INetworkAgent.aidl
@@ -48,4 +48,5 @@
     void onQosCallbackUnregistered(int qosCallbackId);
     void onNetworkCreated();
     void onNetworkDestroyed();
+    void onDscpPolicyStatusUpdated(int policyId, int status);
 }
diff --git a/framework/src/android/net/INetworkAgentRegistry.aidl b/framework/src/android/net/INetworkAgentRegistry.aidl
index 9a58add..08536ca 100644
--- a/framework/src/android/net/INetworkAgentRegistry.aidl
+++ b/framework/src/android/net/INetworkAgentRegistry.aidl
@@ -15,6 +15,7 @@
  */
 package android.net;
 
+import android.net.DscpPolicy;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
@@ -43,4 +44,7 @@
     void sendQosCallbackError(int qosCallbackId, int exceptionType);
     void sendTeardownDelayMs(int teardownDelayMs);
     void sendLingerDuration(int durationMs);
+    void sendAddDscpPolicy(in DscpPolicy policy);
+    void sendRemoveDscpPolicy(int policyId);
+    void sendRemoveAllDscpPolicies();
 }
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index adcf338..945e670 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -25,6 +25,7 @@
 import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
+import android.net.DscpPolicy.DscpPolicyStatus;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.ConditionVariable;
@@ -404,6 +405,35 @@
      */
     public static final int EVENT_LINGER_DURATION_CHANGED = BASE + 24;
 
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to set add a DSCP policy.
+     *
+     * @hide
+     */
+    public static final int EVENT_ADD_DSCP_POLICY = BASE + 25;
+
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to set remove a DSCP policy.
+     *
+     * @hide
+     */
+    public static final int EVENT_REMOVE_DSCP_POLICY = BASE + 26;
+
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to remove all DSCP policies.
+     *
+     * @hide
+     */
+    public static final int EVENT_REMOVE_ALL_DSCP_POLICIES = BASE + 27;
+
+    /**
+     * Sent by ConnectivityService to {@link NetworkAgent} to inform the agent of an updated
+     * status for a DSCP policy.
+     *
+     * @hide
+     */
+    public static final int CMD_DSCP_POLICY_STATUS = BASE + 28;
+
     private static NetworkInfo getLegacyNetworkInfo(final NetworkAgentConfig config) {
         final NetworkInfo ni = new NetworkInfo(config.legacyType, config.legacySubType,
                 config.legacyTypeName, config.legacySubTypeName);
@@ -611,6 +641,12 @@
                     onNetworkDestroyed();
                     break;
                 }
+                case CMD_DSCP_POLICY_STATUS: {
+                    onDscpPolicyStatusUpdated(
+                            msg.arg1 /* Policy ID */,
+                            msg.arg2 /* DSCP Policy Status */);
+                    break;
+                }
             }
         }
     }
@@ -761,6 +797,13 @@
         public void onNetworkDestroyed() {
             mHandler.sendMessage(mHandler.obtainMessage(CMD_NETWORK_DESTROYED));
         }
+
+        @Override
+        public void onDscpPolicyStatusUpdated(final int policyId,
+                @DscpPolicyStatus final int status) {
+            mHandler.sendMessage(mHandler.obtainMessage(
+                    CMD_DSCP_POLICY_STATUS, policyId, status));
+        }
     }
 
     /**
@@ -1104,6 +1147,11 @@
     public void onNetworkDestroyed() {}
 
     /**
+     * Called when when the DSCP Policy status has changed.
+     */
+    public void onDscpPolicyStatusUpdated(int policyId, @DscpPolicyStatus int status) {}
+
+    /**
      * Requests that the network hardware send the specified packet at the specified interval.
      *
      * @param slot the hardware slot on which to start the keepalive.
@@ -1317,6 +1365,30 @@
         queueOrSendMessage(ra -> ra.sendLingerDuration((int) durationMs));
     }
 
+    /**
+     * Add a DSCP Policy.
+     * @param policy the DSCP policy to be added.
+     */
+    public void sendAddDscpPolicy(@NonNull final DscpPolicy policy) {
+        Objects.requireNonNull(policy);
+        queueOrSendMessage(ra -> ra.sendAddDscpPolicy(policy));
+    }
+
+    /**
+     * Remove the specified DSCP policy.
+     * @param policyId the ID corresponding to a specific DSCP Policy.
+     */
+    public void sendRemoveDscpPolicy(final int policyId) {
+        queueOrSendMessage(ra -> ra.sendRemoveDscpPolicy(policyId));
+    }
+
+    /**
+     * Remove all the DSCP policies on this network.
+     */
+    public void sendRemoveAllDscpPolicies() {
+        queueOrSendMessage(ra -> ra.sendRemoveAllDscpPolicies());
+    }
+
     /** @hide */
     protected void log(final String s) {
         Log.d(LOG_TAG, "NetworkAgent: " + s);
diff --git a/framework/src/android/net/NetworkAgentConfig.java b/framework/src/android/net/NetworkAgentConfig.java
index 93fc379..040bf31 100644
--- a/framework/src/android/net/NetworkAgentConfig.java
+++ b/framework/src/android/net/NetworkAgentConfig.java
@@ -425,8 +425,10 @@
          * Sets whether the local traffic is exempted from VPN.
          *
          * @return this builder, to facilitate chaining.
-         * @hide TODO(184750836): Unhide once the implementation is completed.
+         * @hide
          */
+        @NonNull
+        @SystemApi(client = MODULE_LIBRARIES)
         public Builder setExcludeLocalRoutesVpn(boolean excludeLocalRoutes) {
             mConfig.excludeLocalRouteVpn = excludeLocalRoutes;
             return this;
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 84f7cbb..4ae3a06 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -16,8 +16,6 @@
 
 package android.net;
 
-import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
-
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
 
 import android.annotation.IntDef;
@@ -149,67 +147,83 @@
     private String mRequestorPackageName;
 
     /**
-     * enterprise capability sub level 1
-     * @hide
+     * Enterprise capability identifier 1.
      */
-    @SystemApi(client = MODULE_LIBRARIES)
-    public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1 = 1;
+    public static final int NET_ENTERPRISE_ID_1 = 1;
 
     /**
-     * enterprise capability sub level 2
-     * @hide
+     * Enterprise capability identifier 2.
      */
-    @SystemApi(client = MODULE_LIBRARIES)
-    public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2 = 2;
+    public static final int NET_ENTERPRISE_ID_2 = 2;
 
     /**
-     * enterprise capability sub level 3
-     * @hide
+     * Enterprise capability identifier 3.
      */
-    @SystemApi(client = MODULE_LIBRARIES)
-    public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3 = 3;
+    public static final int NET_ENTERPRISE_ID_3 = 3;
 
     /**
-     * enterprise capability sub level 4
-     * @hide
+     * Enterprise capability identifier 4.
      */
-    @SystemApi(client = MODULE_LIBRARIES)
-    public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4 = 4;
+    public static final int NET_ENTERPRISE_ID_4 = 4;
 
     /**
-     * enterprise capability sub level 5
-     * @hide
+     * Enterprise capability identifier 5.
      */
-    @SystemApi(client = MODULE_LIBRARIES)
-    public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5 = 5;
+    public static final int NET_ENTERPRISE_ID_5 = 5;
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = { "NET_CAPABILITY_ENTERPRISE_SUB_LEVEL" }, value = {
-            NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1,
-            NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2,
-            NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3,
-            NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4,
-            NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5,
+            NET_ENTERPRISE_ID_1,
+            NET_ENTERPRISE_ID_2,
+            NET_ENTERPRISE_ID_3,
+            NET_ENTERPRISE_ID_4,
+            NET_ENTERPRISE_ID_5,
     })
 
-    public @interface EnterpriseCapabilitySubLevel {
+    public @interface EnterpriseId {
     }
 
     /**
-     * Bitfield representing the network's enterprise capability sublevel.  If any are specified
+     * Bitfield representing the network's enterprise capability identifier.  If any are specified
      * they will be satisfied by any Network that matches all of them.
-     * {@see addEnterpriseCapabilitySubLevel} for details on how masks are added
+     * {@see addEnterpriseId} for details on how masks are added
      */
-    private int mEnterpriseCapabilitySubLevel;
+    private int mEnterpriseId;
 
     /**
-     * @return all the enteprise capabilities sub level set on this {@code NetworkCapability}
-     * instance.
+     * Get enteprise identifiers set.
+     *
+     * Get all the enterprise capabilities identifier set on this {@code NetworkCapability}
+     * If NET_CAPABILITY_ENTERPRISE is set and no enterprise ID is set, it is
+     * considered to have NET_CAPABILITY_ENTERPRISE by default.
+     * @return all the enterprise capabilities identifier set.
      *
      */
-    public @NonNull @EnterpriseCapabilitySubLevel int[] getEnterpriseCapabilitySubLevels() {
-        return NetworkCapabilitiesUtils.unpackBits(mEnterpriseCapabilitySubLevel);
+    public @NonNull @EnterpriseId int[] getEnterpriseIds() {
+        if (hasCapability(NET_CAPABILITY_ENTERPRISE) && mEnterpriseId == 0) {
+            return new int[]{NET_ENTERPRISE_ID_1};
+        }
+        return NetworkCapabilitiesUtils.unpackBits(mEnterpriseId);
+    }
+
+    /**
+     * Tests for the presence of an enterprise capability identifier on this instance.
+     *
+     * If NET_CAPABILITY_ENTERPRISE is set and no enterprise ID is set, it is
+     * considered to have NET_CAPABILITY_ENTERPRISE by default.
+     * @param enterpriseId the enterprise capability identifier to be tested for.
+     * @return {@code true} if set on this instance.
+     */
+    public boolean hasEnterpriseId(
+            @EnterpriseId int enterpriseId) {
+        if (enterpriseId == NET_ENTERPRISE_ID_1) {
+            if (hasCapability(NET_CAPABILITY_ENTERPRISE) && mEnterpriseId == 0) {
+                return true;
+            }
+        }
+        return isValidEnterpriseId(enterpriseId)
+                && ((mEnterpriseId & (1L << enterpriseId)) != 0);
     }
 
     public NetworkCapabilities() {
@@ -258,7 +272,7 @@
         mRequestorPackageName = null;
         mSubIds = new ArraySet<>();
         mUnderlyingNetworks = null;
-        mEnterpriseCapabilitySubLevel = 0;
+        mEnterpriseId = 0;
     }
 
     /**
@@ -291,7 +305,7 @@
         // mUnderlyingNetworks is an unmodifiable list if non-null, so a defensive copy is not
         // necessary.
         mUnderlyingNetworks = nc.mUnderlyingNetworks;
-        mEnterpriseCapabilitySubLevel = nc.mEnterpriseCapabilitySubLevel;
+        mEnterpriseId = nc.mEnterpriseId;
     }
 
     /**
@@ -343,6 +357,8 @@
             NET_CAPABILITY_BIP,
             NET_CAPABILITY_HEAD_UNIT,
             NET_CAPABILITY_MMTEL,
+            NET_CAPABILITY_PRIORITIZE_LATENCY,
+            NET_CAPABILITY_PRIORITIZE_BANDWIDTH,
     })
     public @interface NetCapability { }
 
@@ -586,8 +602,18 @@
      */
     public static final int NET_CAPABILITY_MMTEL = 33;
 
+    /**
+     * Indicates that this network should be able to prioritize latency for the internet.
+     */
+    public static final int NET_CAPABILITY_PRIORITIZE_LATENCY = 34;
+
+    /**
+     * Indicates that this network should be able to prioritize bandwidth for the internet.
+     */
+    public static final int NET_CAPABILITY_PRIORITIZE_BANDWIDTH = 35;
+
     private static final int MIN_NET_CAPABILITY = NET_CAPABILITY_MMS;
-    private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_MMTEL;
+    private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_PRIORITIZE_BANDWIDTH;
 
     /**
      * Network capabilities that are expected to be mutable, i.e., can change while a particular
@@ -784,34 +810,34 @@
     }
 
     /**
-     * Adds the given enterprise capability sub level to this {@code NetworkCapability} instance.
-     * Note that when searching for a network to satisfy a request, all capabilities sub level
+     * Adds the given enterprise capability identifier to this {@code NetworkCapability} instance.
+     * Note that when searching for a network to satisfy a request, all capabilities identifier
      * requested must be satisfied.
      *
-     * @param enterpriseCapabilitySubLevel the enterprise capability sub level to be added.
+     * @param enterpriseId the enterprise capability identifier to be added.
      * @return This NetworkCapabilities instance, to facilitate chaining.
      * @hide
      */
-    private @NonNull NetworkCapabilities addEnterpriseCapabilitySubLevel(
-            @EnterpriseCapabilitySubLevel int enterpriseCapabilitySubLevel) {
-        checkValidEnterpriseCapabilitySublevel(enterpriseCapabilitySubLevel);
-        mEnterpriseCapabilitySubLevel |= 1 << enterpriseCapabilitySubLevel;
+    public @NonNull NetworkCapabilities addEnterpriseId(
+            @EnterpriseId int enterpriseId) {
+        checkValidEnterpriseId(enterpriseId);
+        mEnterpriseId |= 1 << enterpriseId;
         return this;
     }
 
     /**
-     * Removes (if found) the given enterprise capability sublevel from this
-     * {@code NetworkCapability} instance that were added via addEnterpriseCapabilitySubLevel(int)
+     * Removes (if found) the given enterprise capability identifier from this
+     * {@code NetworkCapability} instance that were added via addEnterpriseId(int)
      *
-     * @param enterpriseCapabilitySubLevel the enterprise capability sublevel to be removed.
+     * @param enterpriseId the enterprise capability identifier to be removed.
      * @return This NetworkCapabilities instance, to facilitate chaining.
      * @hide
      */
-    private @NonNull NetworkCapabilities removeEnterpriseCapabilitySubLevel(
-            @EnterpriseCapabilitySubLevel  int enterpriseCapabilitySubLevel) {
-        checkValidEnterpriseCapabilitySublevel(enterpriseCapabilitySubLevel);
-        final int mask = ~(1 << enterpriseCapabilitySubLevel);
-        mEnterpriseCapabilitySubLevel &= mask;
+    private @NonNull NetworkCapabilities removeEnterpriseId(
+            @EnterpriseId  int enterpriseId) {
+        checkValidEnterpriseId(enterpriseId);
+        final int mask = ~(1 << enterpriseId);
+        mEnterpriseId &= mask;
         return this;
     }
 
@@ -915,16 +941,19 @@
         return null;
     }
 
-    private boolean equalsEnterpriseCapabilitiesSubLevel(@NonNull NetworkCapabilities nc) {
-        return nc.mEnterpriseCapabilitySubLevel == this.mEnterpriseCapabilitySubLevel;
+    private boolean equalsEnterpriseCapabilitiesId(@NonNull NetworkCapabilities nc) {
+        return nc.mEnterpriseId == this.mEnterpriseId;
     }
 
-    private boolean satisfiedByEnterpriseCapabilitiesSubLevel(@NonNull NetworkCapabilities nc) {
-        final int requestedEnterpriseCapabilitiesSubLevel = mEnterpriseCapabilitySubLevel;
-        final int providedEnterpriseCapabailitiesSubLevel = nc.mEnterpriseCapabilitySubLevel;
+    private boolean satisfiedByEnterpriseCapabilitiesId(@NonNull NetworkCapabilities nc) {
+        final int requestedEnterpriseCapabilitiesId = mEnterpriseId;
+        final int providedEnterpriseCapabailitiesId = nc.mEnterpriseId;
 
-        if ((providedEnterpriseCapabailitiesSubLevel & requestedEnterpriseCapabilitiesSubLevel)
-                == requestedEnterpriseCapabilitiesSubLevel) {
+        if ((providedEnterpriseCapabailitiesId & requestedEnterpriseCapabilitiesId)
+                == requestedEnterpriseCapabilitiesId) {
+            return true;
+        } else if (providedEnterpriseCapabailitiesId == 0
+                && (requestedEnterpriseCapabilitiesId == (1L << NET_ENTERPRISE_ID_1))) {
             return true;
         } else {
             return false;
@@ -1836,7 +1865,7 @@
                 && satisfiedByTransportTypes(nc)
                 && (onlyImmutable || satisfiedByLinkBandwidths(nc))
                 && satisfiedBySpecifier(nc)
-                && satisfiedByEnterpriseCapabilitiesSubLevel(nc)
+                && satisfiedByEnterpriseCapabilitiesId(nc)
                 && (onlyImmutable || satisfiedBySignalStrength(nc))
                 && (onlyImmutable || satisfiedByUids(nc))
                 && (onlyImmutable || satisfiedBySSID(nc))
@@ -1940,7 +1969,7 @@
                 && equalsAdministratorUids(that)
                 && equalsSubscriptionIds(that)
                 && equalsUnderlyingNetworks(that)
-                && equalsEnterpriseCapabilitiesSubLevel(that);
+                && equalsEnterpriseCapabilitiesId(that);
     }
 
     @Override
@@ -1965,7 +1994,7 @@
                 + Arrays.hashCode(mAdministratorUids) * 61
                 + Objects.hashCode(mSubIds) * 67
                 + Objects.hashCode(mUnderlyingNetworks) * 71
-                + mEnterpriseCapabilitySubLevel * 73;
+                + mEnterpriseId * 73;
     }
 
     @Override
@@ -2001,7 +2030,7 @@
         dest.writeString(mRequestorPackageName);
         dest.writeIntArray(CollectionUtils.toIntArray(mSubIds));
         dest.writeTypedList(mUnderlyingNetworks);
-        dest.writeInt(mEnterpriseCapabilitySubLevel);
+        dest.writeInt(mEnterpriseId);
     }
 
     public static final @android.annotation.NonNull Creator<NetworkCapabilities> CREATOR =
@@ -2031,7 +2060,7 @@
                     netCap.mSubIds.add(subIdInts[i]);
                 }
                 netCap.setUnderlyingNetworks(in.createTypedArrayList(Network.CREATOR));
-                netCap.mEnterpriseCapabilitySubLevel = in.readInt();
+                netCap.mEnterpriseId = in.readInt();
                 return netCap;
             }
             @Override
@@ -2123,10 +2152,10 @@
             sb.append(" SubscriptionIds: ").append(mSubIds);
         }
 
-        if (0 != mEnterpriseCapabilitySubLevel) {
-            sb.append(" EnterpriseCapabilitySublevel: ");
-            appendStringRepresentationOfBitMaskToStringBuilder(sb, mEnterpriseCapabilitySubLevel,
-                    NetworkCapabilities::enterpriseCapabilitySublevelNameOf, "&");
+        if (0 != mEnterpriseId) {
+            sb.append(" EnterpriseId: ");
+            appendStringRepresentationOfBitMaskToStringBuilder(sb, mEnterpriseId,
+                    NetworkCapabilities::enterpriseIdNameOf, "&");
         }
 
         sb.append(" UnderlyingNetworks: ");
@@ -2224,11 +2253,13 @@
             case NET_CAPABILITY_BIP:                  return "BIP";
             case NET_CAPABILITY_HEAD_UNIT:            return "HEAD_UNIT";
             case NET_CAPABILITY_MMTEL:                return "MMTEL";
+            case NET_CAPABILITY_PRIORITIZE_LATENCY:          return "PRIORITIZE_LATENCY";
+            case NET_CAPABILITY_PRIORITIZE_BANDWIDTH:        return "PRIORITIZE_BANDWIDTH";
             default:                                  return Integer.toString(capability);
         }
     }
 
-    private static @NonNull String enterpriseCapabilitySublevelNameOf(
+    private static @NonNull String enterpriseIdNameOf(
             @NetCapability int capability) {
         return Integer.toString(capability);
     }
@@ -2273,17 +2304,17 @@
         }
     }
 
-    private static boolean isValidEnterpriseCapabilitySubLevel(
-            @NetworkCapabilities.EnterpriseCapabilitySubLevel int enterpriseCapabilitySubLevel) {
-        return enterpriseCapabilitySubLevel >= NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1
-                && enterpriseCapabilitySubLevel <= NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5;
+    private static boolean isValidEnterpriseId(
+            @NetworkCapabilities.EnterpriseId int enterpriseId) {
+        return enterpriseId >= NET_ENTERPRISE_ID_1
+                && enterpriseId <= NET_ENTERPRISE_ID_5;
     }
 
-    private static void checkValidEnterpriseCapabilitySublevel(
-            @NetworkCapabilities.EnterpriseCapabilitySubLevel int enterpriseCapabilitySubLevel) {
-        if (!isValidEnterpriseCapabilitySubLevel(enterpriseCapabilitySubLevel)) {
-            throw new IllegalArgumentException("enterprise capability sublevel "
-                    + enterpriseCapabilitySubLevel + " is out of range");
+    private static void checkValidEnterpriseId(
+            @NetworkCapabilities.EnterpriseId int enterpriseId) {
+        if (!isValidEnterpriseId(enterpriseId)) {
+            throw new IllegalArgumentException("enterprise capability identifier "
+                    + enterpriseId + " is out of range");
         }
     }
 
@@ -2613,33 +2644,33 @@
         }
 
         /**
-         * Adds the given enterprise capability sub level.
-         * Note that when searching for a network to satisfy a request, all capabilities sub level
-         * requested must be satisfied. Enterprise capability sub-level is applicable only
+         * Adds the given enterprise capability identifier.
+         * Note that when searching for a network to satisfy a request, all capabilities identifier
+         * requested must be satisfied. Enterprise capability identifier is applicable only
          * for NET_CAPABILITY_ENTERPRISE capability
          *
-         * @param enterpriseCapabilitySubLevel enterprise capability sub-level.
+         * @param enterpriseId enterprise capability identifier.
          *
          * @return this builder
          */
         @NonNull
-        public Builder addEnterpriseCapabilitySubLevel(
-                @EnterpriseCapabilitySubLevel  int enterpriseCapabilitySubLevel) {
-            mCaps.addEnterpriseCapabilitySubLevel(enterpriseCapabilitySubLevel);
+        public Builder addEnterpriseId(
+                @EnterpriseId  int enterpriseId) {
+            mCaps.addEnterpriseId(enterpriseId);
             return this;
         }
 
         /**
-         * Removes the given enterprise capability sub level. Enterprise capability sub-level is
+         * Removes the given enterprise capability identifier. Enterprise capability identifier is
          * applicable only for NET_CAPABILITY_ENTERPRISE capability
          *
-         * @param enterpriseCapabilitySubLevel the enterprise capability subLevel
+         * @param enterpriseId the enterprise capability identifier
          * @return this builder
          */
         @NonNull
-        public Builder removeEnterpriseCapabilitySubLevel(
-                @EnterpriseCapabilitySubLevel  int enterpriseCapabilitySubLevel) {
-            mCaps.removeEnterpriseCapabilitySubLevel(enterpriseCapabilitySubLevel);
+        public Builder removeEnterpriseId(
+                @EnterpriseId  int enterpriseId) {
+            mCaps.removeEnterpriseId(enterpriseId);
             return this;
         }
 
@@ -2902,10 +2933,10 @@
                 }
             }
 
-            if ((mCaps.getEnterpriseCapabilitySubLevels().length != 0)
+            if ((mCaps.getEnterpriseIds().length != 0)
                     && !mCaps.hasCapability(NET_CAPABILITY_ENTERPRISE)) {
-                throw new IllegalStateException("Enterprise capability sublevel is applicable only"
-                        + " with ENTERPRISE capability.");
+                throw new IllegalStateException("Enterprise capability identifier is applicable"
+                        + " only with ENTERPRISE capability.");
             }
             return new NetworkCapabilities(mCaps);
         }
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index afc76d6..b7a6076 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -725,6 +725,33 @@
     }
 
     /**
+     * Get the enteprise identifiers.
+     *
+     * Get all the enterprise identifiers set on this {@code NetworkCapability}
+     * @return array of all the enterprise identifiers.
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public @NonNull @NetworkCapabilities.EnterpriseId int[] getEnterpriseIds() {
+        // No need to make a defensive copy here as NC#getCapabilities() already returns
+        // a new array.
+        return networkCapabilities.getEnterpriseIds();
+    }
+
+    /**
+     * Tests for the presence of an enterprise identifier on this instance.
+     *
+     * @param enterpriseId the enterprise capability identifier to be tested for.
+     * @return {@code true} if set on this instance.
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public boolean hasEnterpriseId(
+            @NetworkCapabilities.EnterpriseId int enterpriseId) {
+        return networkCapabilities.hasEnterpriseId(enterpriseId);
+    }
+
+    /**
      * Gets all the forbidden capabilities set on this {@code NetworkRequest} instance.
      *
      * @return an array of forbidden capability values for this instance.
diff --git a/framework/src/android/net/ProfileNetworkPreference.java b/framework/src/android/net/ProfileNetworkPreference.java
index 2ce1698..f43acce 100644
--- a/framework/src/android/net/ProfileNetworkPreference.java
+++ b/framework/src/android/net/ProfileNetworkPreference.java
@@ -18,13 +18,20 @@
 
 import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
 import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.net.ConnectivityManager.ProfileNetworkPreferencePolicy;
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
 /**
  * Network preferences to be set for the user profile
  * {@link ProfileNetworkPreferencePolicy}.
@@ -33,23 +40,91 @@
 @SystemApi(client = MODULE_LIBRARIES)
 public final class ProfileNetworkPreference implements Parcelable {
     private final @ProfileNetworkPreferencePolicy int mPreference;
+    private final @NetworkCapabilities.EnterpriseId int mPreferenceEnterpriseId;
+    private final List<Integer> mIncludedUids;
+    private final List<Integer> mExcludedUids;
 
-    private ProfileNetworkPreference(int preference) {
+    private ProfileNetworkPreference(int preference, List<Integer> includedUids,
+            List<Integer> excludedUids,
+            @NetworkCapabilities.EnterpriseId int preferenceEnterpriseId) {
         mPreference = preference;
+        mPreferenceEnterpriseId = preferenceEnterpriseId;
+        if (includedUids != null) {
+            mIncludedUids = new ArrayList<>(includedUids);
+        } else {
+            mIncludedUids = new ArrayList<>();
+        }
+
+        if (excludedUids != null) {
+            mExcludedUids = new ArrayList<>(excludedUids);
+        } else {
+            mExcludedUids = new ArrayList<>();
+        }
     }
 
     private ProfileNetworkPreference(Parcel in) {
         mPreference = in.readInt();
+        mIncludedUids = in.readArrayList(Integer.class.getClassLoader());
+        mExcludedUids = in.readArrayList(Integer.class.getClassLoader());
+        mPreferenceEnterpriseId = in.readInt();
     }
 
     public int getPreference() {
         return mPreference;
     }
 
+    /**
+     * Get the list of UIDs subject to this preference.
+     *
+     * Included UIDs and Excluded UIDs can't both be non-empty.
+     * if both are empty, it means this request applies to all uids in the user profile.
+     * if included is not empty, then only included UIDs are applied.
+     * if excluded is not empty, then it is all uids in the user profile except these UIDs.
+     * @return List of uids included for the profile preference.
+     * {@see #getExcludedUids()}
+     */
+    public @NonNull List<Integer> getIncludedUids() {
+        return new ArrayList<>(mIncludedUids);
+    }
+
+    /**
+     * Get the list of UIDS excluded from this preference.
+     *
+     * <ul>Included UIDs and Excluded UIDs can't both be non-empty.</ul>
+     * <ul>If both are empty, it means this request applies to all uids in the user profile.</ul>
+     * <ul>If included is not empty, then only included UIDs are applied.</ul>
+     * <ul>If excluded is not empty, then it is all uids in the user profile except these UIDs.</ul>
+     * @return List of uids not included for the profile preference.
+     * {@see #getIncludedUids()}
+     */
+    public @NonNull List<Integer> getExcludedUids() {
+        return new ArrayList<>(mExcludedUids);
+    }
+
+    /**
+     * Get preference enterprise identifier.
+     *
+     * Preference enterprise identifier will be used to create different network preferences
+     * within enterprise preference category.
+     * Valid values starts from PROFILE_NETWORK_PREFERENCE_ENTERPRISE_ID_1 to
+     * NetworkCapabilities.NET_ENTERPRISE_ID_5.
+     * Preference identifier is not applicable if preference is set as
+     * PROFILE_NETWORK_PREFERENCE_DEFAULT. Default value is
+     * NetworkCapabilities.NET_ENTERPRISE_ID_1.
+     * @return Preference enterprise identifier.
+     *
+     */
+    public @NetworkCapabilities.EnterpriseId int getPreferenceEnterpriseId() {
+        return mPreferenceEnterpriseId;
+    }
+
     @Override
     public String toString() {
         return "ProfileNetworkPreference{"
                 + "mPreference=" + getPreference()
+                + "mIncludedUids=" + mIncludedUids.toString()
+                + "mExcludedUids=" + mExcludedUids.toString()
+                + "mPreferenceEnterpriseId=" + mPreferenceEnterpriseId
                 + '}';
     }
 
@@ -58,12 +133,18 @@
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
         final ProfileNetworkPreference that = (ProfileNetworkPreference) o;
-        return mPreference == that.mPreference;
+        return mPreference == that.mPreference
+                && (Objects.equals(mIncludedUids, that.mIncludedUids))
+                && (Objects.equals(mExcludedUids, that.mExcludedUids))
+                && mPreferenceEnterpriseId == that.mPreferenceEnterpriseId;
     }
 
     @Override
     public int hashCode() {
-        return mPreference;
+        return mPreference
+                + mPreferenceEnterpriseId * 2
+                + (Objects.hashCode(mIncludedUids) * 11)
+                + (Objects.hashCode(mExcludedUids) * 13);
     }
 
     /**
@@ -73,6 +154,9 @@
     public static final class Builder {
         private @ProfileNetworkPreferencePolicy int mPreference =
                 PROFILE_NETWORK_PREFERENCE_DEFAULT;
+        private @NonNull List<Integer> mIncludedUids = new ArrayList<>();
+        private @NonNull List<Integer> mExcludedUids = new ArrayList<>();
+        private int mPreferenceEnterpriseId;
 
         /**
          * Constructs an empty Builder with PROFILE_NETWORK_PREFERENCE_DEFAULT profile preference
@@ -93,18 +177,112 @@
         }
 
         /**
+         * This is a list of uids for which profile perefence is set.
+         * Null would mean that this preference applies to all uids in the profile.
+         * {@see #setExcludedUids(List<Integer>)}
+         * Included UIDs and Excluded UIDs can't both be non-empty.
+         * if both are empty, it means this request applies to all uids in the user profile.
+         * if included is not empty, then only included UIDs are applied.
+         * if excluded is not empty, then it is all uids in the user profile except these UIDs.
+         * @param uids  list of uids that are included
+         * @return The builder to facilitate chaining.
+         */
+        @NonNull
+        public Builder setIncludedUids(@Nullable List<Integer> uids) {
+            if (uids != null) {
+                mIncludedUids = new ArrayList<Integer>(uids);
+            } else {
+                mIncludedUids = new ArrayList<Integer>();
+            }
+            return this;
+        }
+
+
+        /**
+         * This is a list of uids that are excluded for the profile perefence.
+         * {@see #setIncludedUids(List<Integer>)}
+         * Included UIDs and Excluded UIDs can't both be non-empty.
+         * if both are empty, it means this request applies to all uids in the user profile.
+         * if included is not empty, then only included UIDs are applied.
+         * if excluded is not empty, then it is all uids in the user profile except these UIDs.
+         * @param uids  list of uids that are not included
+         * @return The builder to facilitate chaining.
+         */
+        @NonNull
+        public Builder setExcludedUids(@Nullable List<Integer> uids) {
+            if (uids != null) {
+                mExcludedUids = new ArrayList<Integer>(uids);
+            } else {
+                mExcludedUids = new ArrayList<Integer>();
+            }
+            return this;
+        }
+
+        /**
+         * Check if given preference enterprise identifier is valid
+         *
+         * Valid values starts from PROFILE_NETWORK_PREFERENCE_ENTERPRISE_ID_1 to
+         * NetworkCapabilities.NET_ENTERPRISE_ID_5.
+         * @return True if valid else false
+         * @hide
+         */
+        private boolean isEnterpriseIdentifierValid(
+                @NetworkCapabilities.EnterpriseId int identifier) {
+            if ((identifier >= NET_ENTERPRISE_ID_1)
+                    && (identifier <= NET_ENTERPRISE_ID_5)) {
+                return true;
+            }
+            return false;
+        }
+
+        /**
          * Returns an instance of {@link ProfileNetworkPreference} created from the
          * fields set on this builder.
          */
         @NonNull
         public ProfileNetworkPreference  build() {
-            return new ProfileNetworkPreference(mPreference);
+            if (mIncludedUids.size() > 0 && mExcludedUids.size() > 0) {
+                throw new IllegalArgumentException("Both includedUids and excludedUids "
+                        + "cannot be nonempty");
+            }
+
+            if (((mPreference != PROFILE_NETWORK_PREFERENCE_DEFAULT)
+                    && (!isEnterpriseIdentifierValid(mPreferenceEnterpriseId)))
+                    || ((mPreference == PROFILE_NETWORK_PREFERENCE_DEFAULT)
+                    && (mPreferenceEnterpriseId != 0))) {
+                throw new IllegalStateException("Invalid preference enterprise identifier");
+            }
+            return new ProfileNetworkPreference(mPreference, mIncludedUids,
+                    mExcludedUids, mPreferenceEnterpriseId);
+        }
+
+        /**
+         * Set the preference enterprise identifier.
+         *
+         * Preference enterprise identifier will be used to create different network preferences
+         * within enterprise preference category.
+         * Valid values starts from NetworkCapabilities.NET_ENTERPRISE_ID_1 to
+         * NetworkCapabilities.NET_ENTERPRISE_ID_5.
+         * Preference identifier is not applicable if preference is set as
+         * PROFILE_NETWORK_PREFERENCE_DEFAULT. Default value is
+         * NetworkCapabilities.NET_ENTERPRISE_ID_1.
+         * @param preferenceId  preference sub level
+         * @return The builder to facilitate chaining.
+         */
+        @NonNull
+        public Builder setPreferenceEnterpriseId(
+                @NetworkCapabilities.EnterpriseId int preferenceId) {
+            mPreferenceEnterpriseId = preferenceId;
+            return this;
         }
     }
 
     @Override
     public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
         dest.writeInt(mPreference);
+        dest.writeList(mIncludedUids);
+        dest.writeList(mExcludedUids);
+        dest.writeInt(mPreferenceEnterpriseId);
     }
 
     @Override
diff --git a/service-t/native/libs/libnetworkstats/Android.bp b/service-t/native/libs/libnetworkstats/Android.bp
new file mode 100644
index 0000000..bceeefa
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/Android.bp
@@ -0,0 +1,71 @@
+//
+// Copyright (C) 2017 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library {
+    name: "libnetworkstats",
+    vendor_available: false,
+    host_supported: false,
+    header_libs: ["bpf_connectivity_headers"],
+    srcs: [
+        "BpfNetworkStats.cpp"
+    ],
+    shared_libs: [
+        "libbase",
+        "liblog",
+    ],
+    export_include_dirs: ["include"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+        "-Wthread-safety",
+    ],
+    sanitize: {
+        cfi: true,
+    },
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.tethering",
+    ],
+    min_sdk_version: "30",
+}
+
+cc_test {
+    name: "libnetworkstats_test",
+    test_suites: ["general-tests"],
+    require_root: true,  // required by setrlimitForTest()
+    header_libs: ["bpf_connectivity_headers"],
+    srcs: [
+        "BpfNetworkStatsTest.cpp",
+    ],
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+        "-Wthread-safety",
+    ],
+    static_libs: ["libgmock"],
+    shared_libs: [
+        "libbase",
+        "liblog",
+        "libnetworkstats",
+        "libutils",
+    ],
+}
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
new file mode 100644
index 0000000..4d605ce
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2017 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 <inttypes.h>
+#include <net/if.h>
+#include <string.h>
+#include <unordered_set>
+
+#include <utils/Log.h>
+#include <utils/misc.h>
+
+#include "android-base/file.h"
+#include "android-base/strings.h"
+#include "android-base/unique_fd.h"
+#include "bpf/BpfMap.h"
+#include "bpf_shared.h"
+#include "netdbpf/BpfNetworkStats.h"
+
+#ifdef LOG_TAG
+#undef LOG_TAG
+#endif
+
+#define LOG_TAG "BpfNetworkStats"
+
+namespace android {
+namespace bpf {
+
+using base::Result;
+
+// The target map for stats reading should be the inactive map, which is opposite
+// from the config value.
+static constexpr char const* STATS_MAP_PATH[] = {STATS_MAP_B_PATH, STATS_MAP_A_PATH};
+
+int bpfGetUidStatsInternal(uid_t uid, Stats* stats,
+                           const BpfMap<uint32_t, StatsValue>& appUidStatsMap) {
+    auto statsEntry = appUidStatsMap.readValue(uid);
+    if (statsEntry.ok()) {
+        stats->rxPackets = statsEntry.value().rxPackets;
+        stats->txPackets = statsEntry.value().txPackets;
+        stats->rxBytes = statsEntry.value().rxBytes;
+        stats->txBytes = statsEntry.value().txBytes;
+    }
+    return (statsEntry.ok() || statsEntry.error().code() == ENOENT) ? 0
+                                                                    : -statsEntry.error().code();
+}
+
+int bpfGetUidStats(uid_t uid, Stats* stats) {
+    BpfMapRO<uint32_t, StatsValue> appUidStatsMap(APP_UID_STATS_MAP_PATH);
+
+    if (!appUidStatsMap.isValid()) {
+        int ret = -errno;
+        ALOGE("Opening appUidStatsMap(%s) failed: %s", APP_UID_STATS_MAP_PATH, strerror(errno));
+        return ret;
+    }
+    return bpfGetUidStatsInternal(uid, stats, appUidStatsMap);
+}
+
+int bpfGetIfaceStatsInternal(const char* iface, Stats* stats,
+                             const BpfMap<uint32_t, StatsValue>& ifaceStatsMap,
+                             const BpfMap<uint32_t, IfaceValue>& ifaceNameMap) {
+    int64_t unknownIfaceBytesTotal = 0;
+    stats->tcpRxPackets = -1;
+    stats->tcpTxPackets = -1;
+    const auto processIfaceStats =
+            [iface, stats, &ifaceNameMap, &unknownIfaceBytesTotal](
+                    const uint32_t& key,
+                    const BpfMap<uint32_t, StatsValue>& ifaceStatsMap) -> Result<void> {
+        char ifname[IFNAMSIZ];
+        if (getIfaceNameFromMap(ifaceNameMap, ifaceStatsMap, key, ifname, key,
+                                &unknownIfaceBytesTotal)) {
+            return Result<void>();
+        }
+        if (!iface || !strcmp(iface, ifname)) {
+            Result<StatsValue> statsEntry = ifaceStatsMap.readValue(key);
+            if (!statsEntry.ok()) {
+                return statsEntry.error();
+            }
+            stats->rxPackets += statsEntry.value().rxPackets;
+            stats->txPackets += statsEntry.value().txPackets;
+            stats->rxBytes += statsEntry.value().rxBytes;
+            stats->txBytes += statsEntry.value().txBytes;
+        }
+        return Result<void>();
+    };
+    auto res = ifaceStatsMap.iterate(processIfaceStats);
+    return res.ok() ? 0 : -res.error().code();
+}
+
+int bpfGetIfaceStats(const char* iface, Stats* stats) {
+    BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
+    int ret;
+    if (!ifaceStatsMap.isValid()) {
+        ret = -errno;
+        ALOGE("get ifaceStats map fd failed: %s", strerror(errno));
+        return ret;
+    }
+    BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+    if (!ifaceIndexNameMap.isValid()) {
+        ret = -errno;
+        ALOGE("get ifaceIndexName map fd failed: %s", strerror(errno));
+        return ret;
+    }
+    return bpfGetIfaceStatsInternal(iface, stats, ifaceStatsMap, ifaceIndexNameMap);
+}
+
+stats_line populateStatsEntry(const StatsKey& statsKey, const StatsValue& statsEntry,
+                              const char* ifname) {
+    stats_line newLine;
+    strlcpy(newLine.iface, ifname, sizeof(newLine.iface));
+    newLine.uid = (int32_t)statsKey.uid;
+    newLine.set = (int32_t)statsKey.counterSet;
+    newLine.tag = (int32_t)statsKey.tag;
+    newLine.rxPackets = statsEntry.rxPackets;
+    newLine.txPackets = statsEntry.txPackets;
+    newLine.rxBytes = statsEntry.rxBytes;
+    newLine.txBytes = statsEntry.txBytes;
+    return newLine;
+}
+
+int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>* lines,
+                                       const std::vector<std::string>& limitIfaces, int limitTag,
+                                       int limitUid, const BpfMap<StatsKey, StatsValue>& statsMap,
+                                       const BpfMap<uint32_t, IfaceValue>& ifaceMap) {
+    int64_t unknownIfaceBytesTotal = 0;
+    const auto processDetailUidStats =
+            [lines, &limitIfaces, &limitTag, &limitUid, &unknownIfaceBytesTotal, &ifaceMap](
+                    const StatsKey& key,
+                    const BpfMap<StatsKey, StatsValue>& statsMap) -> Result<void> {
+        char ifname[IFNAMSIZ];
+        if (getIfaceNameFromMap(ifaceMap, statsMap, key.ifaceIndex, ifname, key,
+                                &unknownIfaceBytesTotal)) {
+            return Result<void>();
+        }
+        std::string ifnameStr(ifname);
+        if (limitIfaces.size() > 0 &&
+            std::find(limitIfaces.begin(), limitIfaces.end(), ifnameStr) == limitIfaces.end()) {
+            // Nothing matched; skip this line.
+            return Result<void>();
+        }
+        if (limitTag != TAG_ALL && uint32_t(limitTag) != key.tag) {
+            return Result<void>();
+        }
+        if (limitUid != UID_ALL && uint32_t(limitUid) != key.uid) {
+            return Result<void>();
+        }
+        Result<StatsValue> statsEntry = statsMap.readValue(key);
+        if (!statsEntry.ok()) {
+            return base::ResultError(statsEntry.error().message(), statsEntry.error().code());
+        }
+        lines->push_back(populateStatsEntry(key, statsEntry.value(), ifname));
+        return Result<void>();
+    };
+    Result<void> res = statsMap.iterate(processDetailUidStats);
+    if (!res.ok()) {
+        ALOGE("failed to iterate per uid Stats map for detail traffic stats: %s",
+              strerror(res.error().code()));
+        return -res.error().code();
+    }
+
+    // Since eBPF use hash map to record stats, network stats collected from
+    // eBPF will be out of order. And the performance of findIndexHinted in
+    // NetworkStats will also be impacted.
+    //
+    // Furthermore, since the StatsKey contains iface index, the network stats
+    // reported to framework would create items with the same iface, uid, tag
+    // and set, which causes NetworkStats maps wrong item to subtract.
+    //
+    // Thus, the stats needs to be properly sorted and grouped before reported.
+    groupNetworkStats(lines);
+    return 0;
+}
+
+int parseBpfNetworkStatsDetail(std::vector<stats_line>* lines,
+                               const std::vector<std::string>& limitIfaces, int limitTag,
+                               int limitUid) {
+    BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+    if (!ifaceIndexNameMap.isValid()) {
+        int ret = -errno;
+        ALOGE("get ifaceIndexName map fd failed: %s", strerror(errno));
+        return ret;
+    }
+
+    BpfMapRO<uint32_t, uint8_t> configurationMap(CONFIGURATION_MAP_PATH);
+    if (!configurationMap.isValid()) {
+        int ret = -errno;
+        ALOGE("get configuration map fd failed: %s", strerror(errno));
+        return ret;
+    }
+    auto configuration = configurationMap.readValue(CURRENT_STATS_MAP_CONFIGURATION_KEY);
+    if (!configuration.ok()) {
+        ALOGE("Cannot read the old configuration from map: %s",
+              configuration.error().message().c_str());
+        return -configuration.error().code();
+    }
+    const char* statsMapPath = STATS_MAP_PATH[configuration.value()];
+    BpfMap<StatsKey, StatsValue> statsMap(statsMapPath);
+    if (!statsMap.isValid()) {
+        int ret = -errno;
+        ALOGE("get stats map fd failed: %s, path: %s", strerror(errno), statsMapPath);
+        return ret;
+    }
+
+    // It is safe to read and clear the old map now since the
+    // networkStatsFactory should call netd to swap the map in advance already.
+    int ret = parseBpfNetworkStatsDetailInternal(lines, limitIfaces, limitTag, limitUid, statsMap,
+                                                 ifaceIndexNameMap);
+    if (ret) {
+        ALOGE("parse detail network stats failed: %s", strerror(errno));
+        return ret;
+    }
+
+    Result<void> res = statsMap.clear();
+    if (!res.ok()) {
+        ALOGE("Clean up current stats map failed: %s", strerror(res.error().code()));
+        return -res.error().code();
+    }
+
+    return 0;
+}
+
+int parseBpfNetworkStatsDevInternal(std::vector<stats_line>* lines,
+                                    const BpfMap<uint32_t, StatsValue>& statsMap,
+                                    const BpfMap<uint32_t, IfaceValue>& ifaceMap) {
+    int64_t unknownIfaceBytesTotal = 0;
+    const auto processDetailIfaceStats = [lines, &unknownIfaceBytesTotal, &ifaceMap, &statsMap](
+                                             const uint32_t& key, const StatsValue& value,
+                                             const BpfMap<uint32_t, StatsValue>&) {
+        char ifname[IFNAMSIZ];
+        if (getIfaceNameFromMap(ifaceMap, statsMap, key, ifname, key, &unknownIfaceBytesTotal)) {
+            return Result<void>();
+        }
+        StatsKey fakeKey = {
+                .uid = (uint32_t)UID_ALL,
+                .tag = (uint32_t)TAG_NONE,
+                .counterSet = (uint32_t)SET_ALL,
+        };
+        lines->push_back(populateStatsEntry(fakeKey, value, ifname));
+        return Result<void>();
+    };
+    Result<void> res = statsMap.iterateWithValue(processDetailIfaceStats);
+    if (!res.ok()) {
+        ALOGE("failed to iterate per uid Stats map for detail traffic stats: %s",
+              strerror(res.error().code()));
+        return -res.error().code();
+    }
+
+    groupNetworkStats(lines);
+    return 0;
+}
+
+int parseBpfNetworkStatsDev(std::vector<stats_line>* lines) {
+    int ret = 0;
+    BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+    if (!ifaceIndexNameMap.isValid()) {
+        ret = -errno;
+        ALOGE("get ifaceIndexName map fd failed: %s", strerror(errno));
+        return ret;
+    }
+
+    BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
+    if (!ifaceStatsMap.isValid()) {
+        ret = -errno;
+        ALOGE("get ifaceStats map fd failed: %s", strerror(errno));
+        return ret;
+    }
+    return parseBpfNetworkStatsDevInternal(lines, ifaceStatsMap, ifaceIndexNameMap);
+}
+
+uint64_t combineUidTag(const uid_t uid, const uint32_t tag) {
+    return (uint64_t)uid << 32 | tag;
+}
+
+void groupNetworkStats(std::vector<stats_line>* lines) {
+    if (lines->size() <= 1) return;
+    std::sort(lines->begin(), lines->end());
+
+    // Similar to std::unique(), but aggregates the duplicates rather than discarding them.
+    size_t nextOutput = 0;
+    for (size_t i = 1; i < lines->size(); i++) {
+        if (lines->at(nextOutput) == lines->at(i)) {
+            lines->at(nextOutput) += lines->at(i);
+        } else {
+            nextOutput++;
+            if (nextOutput != i) {
+                lines->at(nextOutput) = lines->at(i);
+            }
+        }
+    }
+
+    if (lines->size() != nextOutput + 1) {
+        lines->resize(nextOutput + 1);
+    }
+}
+
+// True if lhs equals to rhs, only compare iface, uid, tag and set.
+bool operator==(const stats_line& lhs, const stats_line& rhs) {
+    return ((lhs.uid == rhs.uid) && (lhs.tag == rhs.tag) && (lhs.set == rhs.set) &&
+            !strncmp(lhs.iface, rhs.iface, sizeof(lhs.iface)));
+}
+
+// True if lhs is smaller than rhs, only compare iface, uid, tag and set.
+bool operator<(const stats_line& lhs, const stats_line& rhs) {
+    int ret = strncmp(lhs.iface, rhs.iface, sizeof(lhs.iface));
+    if (ret != 0) return ret < 0;
+    if (lhs.uid < rhs.uid) return true;
+    if (lhs.uid > rhs.uid) return false;
+    if (lhs.tag < rhs.tag) return true;
+    if (lhs.tag > rhs.tag) return false;
+    if (lhs.set < rhs.set) return true;
+    if (lhs.set > rhs.set) return false;
+    return false;
+}
+
+stats_line& stats_line::operator=(const stats_line& rhs) {
+    if (this == &rhs) return *this;
+
+    strlcpy(iface, rhs.iface, sizeof(iface));
+    uid = rhs.uid;
+    set = rhs.set;
+    tag = rhs.tag;
+    rxPackets = rhs.rxPackets;
+    txPackets = rhs.txPackets;
+    rxBytes = rhs.rxBytes;
+    txBytes = rhs.txBytes;
+    return *this;
+}
+
+stats_line& stats_line::operator+=(const stats_line& rhs) {
+    rxPackets += rhs.rxPackets;
+    txPackets += rhs.txPackets;
+    rxBytes += rhs.rxBytes;
+    txBytes += rhs.txBytes;
+    return *this;
+}
+
+}  // namespace bpf
+}  // namespace android
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
new file mode 100644
index 0000000..4974b96
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
@@ -0,0 +1,569 @@
+/*
+ * Copyright (C) 2018 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 <fstream>
+#include <iostream>
+#include <string>
+#include <vector>
+
+#include <fcntl.h>
+#include <inttypes.h>
+#include <linux/inet_diag.h>
+#include <linux/sock_diag.h>
+#include <net/if.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <gtest/gtest.h>
+
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+
+#include "bpf/BpfMap.h"
+#include "bpf/BpfUtils.h"
+#include "netdbpf/BpfNetworkStats.h"
+
+using ::testing::Test;
+
+namespace android {
+namespace bpf {
+
+using base::Result;
+using base::unique_fd;
+
+constexpr int TEST_MAP_SIZE = 10;
+constexpr uid_t TEST_UID1 = 10086;
+constexpr uid_t TEST_UID2 = 12345;
+constexpr uint32_t TEST_TAG = 42;
+constexpr int TEST_COUNTERSET0 = 0;
+constexpr int TEST_COUNTERSET1 = 1;
+constexpr uint64_t TEST_BYTES0 = 1000;
+constexpr uint64_t TEST_BYTES1 = 2000;
+constexpr uint64_t TEST_PACKET0 = 100;
+constexpr uint64_t TEST_PACKET1 = 200;
+constexpr const char IFACE_NAME1[] = "lo";
+constexpr const char IFACE_NAME2[] = "wlan0";
+constexpr const char IFACE_NAME3[] = "rmnet_data0";
+// A iface name that the size is bigger than IFNAMSIZ
+constexpr const char LONG_IFACE_NAME[] = "wlanWithALongName";
+constexpr const char TRUNCATED_IFACE_NAME[] = "wlanWithALongNa";
+constexpr uint32_t IFACE_INDEX1 = 1;
+constexpr uint32_t IFACE_INDEX2 = 2;
+constexpr uint32_t IFACE_INDEX3 = 3;
+constexpr uint32_t IFACE_INDEX4 = 4;
+constexpr uint32_t UNKNOWN_IFACE = 0;
+
+class BpfNetworkStatsHelperTest : public testing::Test {
+  protected:
+    BpfNetworkStatsHelperTest() {}
+    BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap;
+    BpfMap<uint32_t, StatsValue> mFakeAppUidStatsMap;
+    BpfMap<StatsKey, StatsValue> mFakeStatsMap;
+    BpfMap<uint32_t, IfaceValue> mFakeIfaceIndexNameMap;
+    BpfMap<uint32_t, StatsValue> mFakeIfaceStatsMap;
+
+    void SetUp() {
+        ASSERT_EQ(0, setrlimitForTest());
+
+        mFakeCookieTagMap = BpfMap<uint64_t, UidTagValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        ASSERT_LE(0, mFakeCookieTagMap.getMap());
+
+        mFakeAppUidStatsMap = BpfMap<uint32_t, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        ASSERT_LE(0, mFakeAppUidStatsMap.getMap());
+
+        mFakeStatsMap = BpfMap<StatsKey, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        ASSERT_LE(0, mFakeStatsMap.getMap());
+
+        mFakeIfaceIndexNameMap = BpfMap<uint32_t, IfaceValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        ASSERT_LE(0, mFakeIfaceIndexNameMap.getMap());
+
+        mFakeIfaceStatsMap = BpfMap<uint32_t, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        ASSERT_LE(0, mFakeIfaceStatsMap.getMap());
+    }
+
+    void expectUidTag(uint64_t cookie, uid_t uid, uint32_t tag) {
+        auto tagResult = mFakeCookieTagMap.readValue(cookie);
+        EXPECT_RESULT_OK(tagResult);
+        EXPECT_EQ(uid, tagResult.value().uid);
+        EXPECT_EQ(tag, tagResult.value().tag);
+    }
+
+    void populateFakeStats(uid_t uid, uint32_t tag, uint32_t ifaceIndex, uint32_t counterSet,
+                           StatsValue value, BpfMap<StatsKey, StatsValue>& map) {
+        StatsKey key = {
+            .uid = (uint32_t)uid, .tag = tag, .counterSet = counterSet, .ifaceIndex = ifaceIndex};
+        EXPECT_RESULT_OK(map.writeValue(key, value, BPF_ANY));
+    }
+
+    void updateIfaceMap(const char* ifaceName, uint32_t ifaceIndex) {
+        IfaceValue iface;
+        strlcpy(iface.name, ifaceName, IFNAMSIZ);
+        EXPECT_RESULT_OK(mFakeIfaceIndexNameMap.writeValue(ifaceIndex, iface, BPF_ANY));
+    }
+
+    void expectStatsEqual(const StatsValue& target, const Stats& result) {
+        EXPECT_EQ(target.rxPackets, result.rxPackets);
+        EXPECT_EQ(target.rxBytes, result.rxBytes);
+        EXPECT_EQ(target.txPackets, result.txPackets);
+        EXPECT_EQ(target.txBytes, result.txBytes);
+    }
+
+    void expectStatsLineEqual(const StatsValue target, const char* iface, uint32_t uid,
+                              int counterSet, uint32_t tag, const stats_line& result) {
+        EXPECT_EQ(0, strcmp(iface, result.iface));
+        EXPECT_EQ(uid, (uint32_t)result.uid);
+        EXPECT_EQ((uint32_t) counterSet, result.set);
+        EXPECT_EQ(tag, (uint32_t)result.tag);
+        EXPECT_EQ(target.rxPackets, (uint64_t)result.rxPackets);
+        EXPECT_EQ(target.rxBytes, (uint64_t)result.rxBytes);
+        EXPECT_EQ(target.txPackets, (uint64_t)result.txPackets);
+        EXPECT_EQ(target.txBytes, (uint64_t)result.txBytes);
+    }
+};
+
+// TEST to verify the behavior of bpf map when cocurrent deletion happens when
+// iterating the same map.
+TEST_F(BpfNetworkStatsHelperTest, TestIterateMapWithDeletion) {
+    for (int i = 0; i < 5; i++) {
+        uint64_t cookie = i + 1;
+        UidTagValue tag = {.uid = TEST_UID1, .tag = TEST_TAG};
+        EXPECT_RESULT_OK(mFakeCookieTagMap.writeValue(cookie, tag, BPF_ANY));
+    }
+    uint64_t curCookie = 0;
+    auto nextCookie = mFakeCookieTagMap.getNextKey(curCookie);
+    EXPECT_RESULT_OK(nextCookie);
+    uint64_t headOfMap = nextCookie.value();
+    curCookie = nextCookie.value();
+    // Find the second entry in the map, then immediately delete it.
+    nextCookie = mFakeCookieTagMap.getNextKey(curCookie);
+    EXPECT_RESULT_OK(nextCookie);
+    EXPECT_RESULT_OK(mFakeCookieTagMap.deleteValue((nextCookie.value())));
+    // Find the entry that is now immediately after headOfMap, then delete that.
+    nextCookie = mFakeCookieTagMap.getNextKey(curCookie);
+    EXPECT_RESULT_OK(nextCookie);
+    EXPECT_RESULT_OK(mFakeCookieTagMap.deleteValue((nextCookie.value())));
+    // Attempting to read an entry that has been deleted fails with ENOENT.
+    curCookie = nextCookie.value();
+    auto tagResult = mFakeCookieTagMap.readValue(curCookie);
+    EXPECT_EQ(ENOENT, tagResult.error().code());
+    // Finding the entry after our deleted entry restarts iteration from the beginning of the map.
+    nextCookie = mFakeCookieTagMap.getNextKey(curCookie);
+    EXPECT_RESULT_OK(nextCookie);
+    EXPECT_EQ(headOfMap, nextCookie.value());
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestBpfIterateMap) {
+    for (int i = 0; i < 5; i++) {
+        uint64_t cookie = i + 1;
+        UidTagValue tag = {.uid = TEST_UID1, .tag = TEST_TAG};
+        EXPECT_RESULT_OK(mFakeCookieTagMap.writeValue(cookie, tag, BPF_ANY));
+    }
+    int totalCount = 0;
+    int totalSum = 0;
+    const auto iterateWithoutDeletion =
+            [&totalCount, &totalSum](const uint64_t& key, const BpfMap<uint64_t, UidTagValue>&) {
+                EXPECT_GE((uint64_t)5, key);
+                totalCount++;
+                totalSum += key;
+                return Result<void>();
+            };
+    EXPECT_RESULT_OK(mFakeCookieTagMap.iterate(iterateWithoutDeletion));
+    EXPECT_EQ(5, totalCount);
+    EXPECT_EQ(1 + 2 + 3 + 4 + 5, totalSum);
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestUidStatsNoTraffic) {
+    StatsValue value1 = {
+            .rxPackets = 0,
+            .rxBytes = 0,
+            .txPackets = 0,
+            .txBytes = 0,
+    };
+    Stats result1 = {};
+    ASSERT_EQ(0, bpfGetUidStatsInternal(TEST_UID1, &result1, mFakeAppUidStatsMap));
+    expectStatsEqual(value1, result1);
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetUidStatsTotal) {
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+    updateIfaceMap(IFACE_NAME3, IFACE_INDEX3);
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1,
+    };
+    StatsValue value2 = {
+            .rxPackets = TEST_PACKET0 * 2,
+            .rxBytes = TEST_BYTES0 * 2,
+            .txPackets = TEST_PACKET1 * 2,
+            .txBytes = TEST_BYTES1 * 2,
+    };
+    ASSERT_RESULT_OK(mFakeAppUidStatsMap.writeValue(TEST_UID1, value1, BPF_ANY));
+    ASSERT_RESULT_OK(mFakeAppUidStatsMap.writeValue(TEST_UID2, value2, BPF_ANY));
+    Stats result1 = {};
+    ASSERT_EQ(0, bpfGetUidStatsInternal(TEST_UID1, &result1, mFakeAppUidStatsMap));
+    expectStatsEqual(value1, result1);
+
+    Stats result2 = {};
+    ASSERT_EQ(0, bpfGetUidStatsInternal(TEST_UID2, &result2, mFakeAppUidStatsMap));
+    expectStatsEqual(value2, result2);
+    std::vector<stats_line> lines;
+    std::vector<std::string> ifaces;
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX2, TEST_COUNTERSET1, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID2, 0, IFACE_INDEX3, TEST_COUNTERSET1, value1, mFakeStatsMap);
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID1,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)2, lines.size());
+    lines.clear();
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID2,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)1, lines.size());
+    expectStatsLineEqual(value1, IFACE_NAME3, TEST_UID2, TEST_COUNTERSET1, 0, lines.front());
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetIfaceStatsInternal) {
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+    updateIfaceMap(IFACE_NAME3, IFACE_INDEX3);
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1,
+    };
+    StatsValue value2 = {
+            .rxPackets = TEST_PACKET1,
+            .rxBytes = TEST_BYTES1,
+            .txPackets = TEST_PACKET0,
+            .txBytes = TEST_BYTES0,
+    };
+    uint32_t ifaceStatsKey = IFACE_INDEX1;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+    ifaceStatsKey = IFACE_INDEX2;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value2, BPF_ANY));
+    ifaceStatsKey = IFACE_INDEX3;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+
+    Stats result1 = {};
+    ASSERT_EQ(0, bpfGetIfaceStatsInternal(IFACE_NAME1, &result1, mFakeIfaceStatsMap,
+                                          mFakeIfaceIndexNameMap));
+    expectStatsEqual(value1, result1);
+    Stats result2 = {};
+    ASSERT_EQ(0, bpfGetIfaceStatsInternal(IFACE_NAME2, &result2, mFakeIfaceStatsMap,
+                                          mFakeIfaceIndexNameMap));
+    expectStatsEqual(value2, result2);
+    Stats totalResult = {};
+    ASSERT_EQ(0, bpfGetIfaceStatsInternal(NULL, &totalResult, mFakeIfaceStatsMap,
+                                          mFakeIfaceIndexNameMap));
+    StatsValue totalValue = {
+            .rxPackets = TEST_PACKET0 * 2 + TEST_PACKET1,
+            .rxBytes = TEST_BYTES0 * 2 + TEST_BYTES1,
+            .txPackets = TEST_PACKET1 * 2 + TEST_PACKET0,
+            .txBytes = TEST_BYTES1 * 2 + TEST_BYTES0,
+    };
+    expectStatsEqual(totalValue, totalResult);
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetStatsDetail) {
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1,
+    };
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX2, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, TEST_TAG + 1, IFACE_INDEX1, TEST_COUNTERSET0, value1,
+                      mFakeStatsMap);
+    populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    std::vector<stats_line> lines;
+    std::vector<std::string> ifaces;
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)4, lines.size());
+    lines.clear();
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID1,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)3, lines.size());
+    lines.clear();
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TEST_TAG, TEST_UID1,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)2, lines.size());
+    lines.clear();
+    ifaces.push_back(std::string(IFACE_NAME1));
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TEST_TAG, TEST_UID1,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)1, lines.size());
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines.front());
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetStatsWithSkippedIface) {
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1,
+    };
+    populateFakeStats(0, 0, 0, OVERFLOW_COUNTERSET, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX2, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET1, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID2, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    std::vector<stats_line> lines;
+    std::vector<std::string> ifaces;
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)4, lines.size());
+    lines.clear();
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID1,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)3, lines.size());
+    lines.clear();
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID2,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)1, lines.size());
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID2, TEST_COUNTERSET0, 0, lines.front());
+    lines.clear();
+    ifaces.push_back(std::string(IFACE_NAME1));
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID1,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)2, lines.size());
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestUnknownIfaceError) {
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0 * 20,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1 * 20,
+    };
+    uint32_t ifaceIndex = UNKNOWN_IFACE;
+    populateFakeStats(TEST_UID1, 0, ifaceIndex, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    StatsValue value2 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0 * 40,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1 * 40,
+    };
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX2, TEST_COUNTERSET0, value2, mFakeStatsMap);
+    StatsKey curKey = {
+            .uid = TEST_UID1,
+            .tag = 0,
+            .counterSet = TEST_COUNTERSET0,
+            .ifaceIndex = ifaceIndex,
+    };
+    char ifname[IFNAMSIZ];
+    int64_t unknownIfaceBytesTotal = 0;
+    ASSERT_EQ(-ENODEV, getIfaceNameFromMap(mFakeIfaceIndexNameMap, mFakeStatsMap, ifaceIndex,
+                                           ifname, curKey, &unknownIfaceBytesTotal));
+    ASSERT_EQ(((int64_t)(TEST_BYTES0 * 20 + TEST_BYTES1 * 20)), unknownIfaceBytesTotal);
+    curKey.ifaceIndex = IFACE_INDEX2;
+    ASSERT_EQ(-ENODEV, getIfaceNameFromMap(mFakeIfaceIndexNameMap, mFakeStatsMap, ifaceIndex,
+                                           ifname, curKey, &unknownIfaceBytesTotal));
+    ASSERT_EQ(-1, unknownIfaceBytesTotal);
+    std::vector<stats_line> lines;
+    std::vector<std::string> ifaces;
+    // TODO: find a way to test the total of unknown Iface Bytes go above limit.
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)1, lines.size());
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, 0, lines.front());
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetIfaceStatsDetail) {
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+    updateIfaceMap(IFACE_NAME3, IFACE_INDEX3);
+    updateIfaceMap(LONG_IFACE_NAME, IFACE_INDEX4);
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1,
+    };
+    StatsValue value2 = {
+            .rxPackets = TEST_PACKET1,
+            .rxBytes = TEST_BYTES1,
+            .txPackets = TEST_PACKET0,
+            .txBytes = TEST_BYTES0,
+    };
+    uint32_t ifaceStatsKey = IFACE_INDEX1;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+    ifaceStatsKey = IFACE_INDEX2;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value2, BPF_ANY));
+    ifaceStatsKey = IFACE_INDEX3;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+    ifaceStatsKey = IFACE_INDEX4;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value2, BPF_ANY));
+    std::vector<stats_line> lines;
+    ASSERT_EQ(0,
+              parseBpfNetworkStatsDevInternal(&lines, mFakeIfaceStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)4, lines.size());
+
+    expectStatsLineEqual(value1, IFACE_NAME1, UID_ALL, SET_ALL, TAG_NONE, lines[0]);
+    expectStatsLineEqual(value1, IFACE_NAME3, UID_ALL, SET_ALL, TAG_NONE, lines[1]);
+    expectStatsLineEqual(value2, IFACE_NAME2, UID_ALL, SET_ALL, TAG_NONE, lines[2]);
+    ASSERT_EQ(0, strcmp(TRUNCATED_IFACE_NAME, lines[3].iface));
+    expectStatsLineEqual(value2, TRUNCATED_IFACE_NAME, UID_ALL, SET_ALL, TAG_NONE, lines[3]);
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetStatsSortedAndGrouped) {
+    // Create iface indexes with duplicate iface name.
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX3);  // Duplicate!
+
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1,
+    };
+    StatsValue value2 = {
+            .rxPackets = TEST_PACKET1,
+            .rxBytes = TEST_BYTES1,
+            .txPackets = TEST_PACKET0,
+            .txBytes = TEST_BYTES0,
+    };
+    StatsValue value3 = {
+            .rxPackets = TEST_PACKET0 * 2,
+            .rxBytes = TEST_BYTES0 * 2,
+            .txPackets = TEST_PACKET1 * 2,
+            .txBytes = TEST_BYTES1 * 2,
+    };
+
+    std::vector<stats_line> lines;
+    std::vector<std::string> ifaces;
+
+    // Test empty stats.
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 0, lines.size());
+    lines.clear();
+
+    // Test 1 line stats.
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 1, lines.size());
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[0]);
+    lines.clear();
+
+    // These items should not be grouped.
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX2, TEST_COUNTERSET0, value2, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET1, value2, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, TEST_TAG + 1, IFACE_INDEX1, TEST_COUNTERSET0, value2,
+                      mFakeStatsMap);
+    populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 5, lines.size());
+    lines.clear();
+
+    // These items should be grouped.
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET0, value1, mFakeStatsMap);
+
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 5, lines.size());
+
+    // Verify Sorted & Grouped.
+    expectStatsLineEqual(value3, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[0]);
+    expectStatsLineEqual(value2, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET1, TEST_TAG, lines[1]);
+    expectStatsLineEqual(value2, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG + 1, lines[2]);
+    expectStatsLineEqual(value3, IFACE_NAME1, TEST_UID2, TEST_COUNTERSET0, TEST_TAG, lines[3]);
+    expectStatsLineEqual(value2, IFACE_NAME2, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[4]);
+    lines.clear();
+
+    // Perform test on IfaceStats.
+    uint32_t ifaceStatsKey = IFACE_INDEX2;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value2, BPF_ANY));
+    ifaceStatsKey = IFACE_INDEX1;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+
+    // This should be grouped.
+    ifaceStatsKey = IFACE_INDEX3;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+
+    ASSERT_EQ(0,
+              parseBpfNetworkStatsDevInternal(&lines, mFakeIfaceStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 2, lines.size());
+
+    expectStatsLineEqual(value3, IFACE_NAME1, UID_ALL, SET_ALL, TAG_NONE, lines[0]);
+    expectStatsLineEqual(value2, IFACE_NAME2, UID_ALL, SET_ALL, TAG_NONE, lines[1]);
+    lines.clear();
+}
+
+// Test to verify that subtract overflow will not be triggered by the compare function invoked from
+// sorting. See http:/b/119193941.
+TEST_F(BpfNetworkStatsHelperTest, TestGetStatsSortAndOverflow) {
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1,
+    };
+
+    // Mutate uid, 0 < TEST_UID1 < INT_MAX < INT_MIN < UINT_MAX.
+    populateFakeStats(0, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(UINT_MAX, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(INT_MIN, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(INT_MAX, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+
+    // Mutate tag, 0 < TEST_TAG < INT_MAX < INT_MIN < UINT_MAX.
+    populateFakeStats(TEST_UID1, INT_MAX, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, INT_MIN, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, UINT_MAX, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+
+    // TODO: Mutate counterSet and enlarge TEST_MAP_SIZE if overflow on counterSet is possible.
+
+    std::vector<stats_line> lines;
+    std::vector<std::string> ifaces;
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 8, lines.size());
+
+    // Uid 0 first
+    expectStatsLineEqual(value1, IFACE_NAME1, 0, TEST_COUNTERSET0, TEST_TAG, lines[0]);
+
+    // Test uid, mutate tag.
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, 0, lines[1]);
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, INT_MAX, lines[2]);
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, INT_MIN, lines[3]);
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, UINT_MAX, lines[4]);
+
+    // Mutate uid.
+    expectStatsLineEqual(value1, IFACE_NAME1, INT_MAX, TEST_COUNTERSET0, TEST_TAG, lines[5]);
+    expectStatsLineEqual(value1, IFACE_NAME1, INT_MIN, TEST_COUNTERSET0, TEST_TAG, lines[6]);
+    expectStatsLineEqual(value1, IFACE_NAME1, UINT_MAX, TEST_COUNTERSET0, TEST_TAG, lines[7]);
+    lines.clear();
+}
+}  // namespace bpf
+}  // namespace android
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
new file mode 100644
index 0000000..8ab7e25
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+#ifndef _BPF_NETWORKSTATS_H
+#define _BPF_NETWORKSTATS_H
+
+#include <bpf/BpfMap.h>
+#include "bpf_shared.h"
+
+namespace android {
+namespace bpf {
+
+// TODO: set this to a proper value based on the map size;
+constexpr int TAG_STATS_MAP_SOFT_LIMIT = 3;
+constexpr int UID_ALL = -1;
+constexpr int TAG_ALL = -1;
+constexpr int TAG_NONE = 0;
+constexpr int SET_ALL = -1;
+constexpr int SET_DEFAULT = 0;
+constexpr int SET_FOREGROUND = 1;
+
+// The limit for stats received by a unknown interface;
+constexpr const int64_t MAX_UNKNOWN_IFACE_BYTES = 100 * 1000;
+
+// This is used by
+// frameworks/base/core/jni/com_android_internal_net_NetworkStatsFactory.cpp
+// make sure it is consistent with the JNI code before changing this.
+struct stats_line {
+    char iface[32];
+    uint32_t uid;
+    uint32_t set;
+    uint32_t tag;
+    int64_t rxBytes;
+    int64_t rxPackets;
+    int64_t txBytes;
+    int64_t txPackets;
+
+    stats_line& operator=(const stats_line& rhs);
+    stats_line& operator+=(const stats_line& rhs);
+};
+
+bool operator==(const stats_line& lhs, const stats_line& rhs);
+bool operator<(const stats_line& lhs, const stats_line& rhs);
+
+// For test only
+int bpfGetUidStatsInternal(uid_t uid, Stats* stats,
+                           const BpfMap<uint32_t, StatsValue>& appUidStatsMap);
+// For test only
+int bpfGetIfaceStatsInternal(const char* iface, Stats* stats,
+                             const BpfMap<uint32_t, StatsValue>& ifaceStatsMap,
+                             const BpfMap<uint32_t, IfaceValue>& ifaceNameMap);
+// For test only
+int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>* lines,
+                                       const std::vector<std::string>& limitIfaces, int limitTag,
+                                       int limitUid, const BpfMap<StatsKey, StatsValue>& statsMap,
+                                       const BpfMap<uint32_t, IfaceValue>& ifaceMap);
+// For test only
+int cleanStatsMapInternal(const base::unique_fd& cookieTagMap, const base::unique_fd& tagStatsMap);
+// For test only
+template <class Key>
+int getIfaceNameFromMap(const BpfMap<uint32_t, IfaceValue>& ifaceMap,
+                        const BpfMap<Key, StatsValue>& statsMap, uint32_t ifaceIndex, char* ifname,
+                        const Key& curKey, int64_t* unknownIfaceBytesTotal) {
+    auto iface = ifaceMap.readValue(ifaceIndex);
+    if (!iface.ok()) {
+        maybeLogUnknownIface(ifaceIndex, statsMap, curKey, unknownIfaceBytesTotal);
+        return -ENODEV;
+    }
+    strlcpy(ifname, iface.value().name, sizeof(IfaceValue));
+    return 0;
+}
+
+template <class Key>
+void maybeLogUnknownIface(int ifaceIndex, const BpfMap<Key, StatsValue>& statsMap,
+                          const Key& curKey, int64_t* unknownIfaceBytesTotal) {
+    // Have we already logged an error?
+    if (*unknownIfaceBytesTotal == -1) {
+        return;
+    }
+
+    // Are we undercounting enough data to be worth logging?
+    auto statsEntry = statsMap.readValue(curKey);
+    if (!statsEntry.ok()) {
+        // No data is being undercounted.
+        return;
+    }
+
+    *unknownIfaceBytesTotal += (statsEntry.value().rxBytes + statsEntry.value().txBytes);
+    if (*unknownIfaceBytesTotal >= MAX_UNKNOWN_IFACE_BYTES) {
+        ALOGE("Unknown name for ifindex %d with more than %" PRId64 " bytes of traffic", ifaceIndex,
+              *unknownIfaceBytesTotal);
+        *unknownIfaceBytesTotal = -1;
+    }
+}
+
+// For test only
+int parseBpfNetworkStatsDevInternal(std::vector<stats_line>* lines,
+                                    const BpfMap<uint32_t, StatsValue>& statsMap,
+                                    const BpfMap<uint32_t, IfaceValue>& ifaceMap);
+
+int bpfGetUidStats(uid_t uid, Stats* stats);
+int bpfGetIfaceStats(const char* iface, Stats* stats);
+int parseBpfNetworkStatsDetail(std::vector<stats_line>* lines,
+                               const std::vector<std::string>& limitIfaces, int limitTag,
+                               int limitUid);
+
+int parseBpfNetworkStatsDev(std::vector<stats_line>* lines);
+void groupNetworkStats(std::vector<stats_line>* lines);
+int cleanStatsMap();
+}  // namespace bpf
+}  // namespace android
+
+#endif  // _BPF_NETWORKSTATS_H
diff --git a/service/Android.bp b/service/Android.bp
index 76f9153..33d4b66 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -36,6 +36,7 @@
     stl: "libc++_static",
     static_libs: [
         "libnet_utils_device_common_bpfjni",
+        "libtcutils",
     ],
     shared_libs: [
         "liblog",
@@ -57,12 +58,19 @@
     ],
     srcs: [
         "jni/com_android_server_TestNetworkService.cpp",
+        "jni/com_android_server_connectivity_ClatCoordinator.cpp",
         "jni/onload.cpp",
     ],
     stl: "libc++_static",
     header_libs: [
         "libbase_headers",
     ],
+    static_libs: [
+        "libbase",
+        "libclat",
+        "libip_checksum",
+        "libnetjniutils",
+    ],
     shared_libs: [
         "liblog",
         "libnativehelper",
@@ -103,6 +111,7 @@
         "networkstack-client",
         "PlatformProperties",
         "service-connectivity-protos",
+        "NetworkStackApiCurrentShims",
     ],
     apex_available: [
         "com.android.tethering",
@@ -156,3 +165,11 @@
     srcs: ["jarjar-rules.txt"],
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
+
+// TODO: This filegroup temporary exposes for NetworkStats. It should be
+// removed right after NetworkStats moves into mainline module.
+filegroup {
+    name: "traffic-controller-utils",
+    srcs: ["src/com/android/server/BpfNetMaps.java"],
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
diff --git a/service/jarjar-rules.txt b/service/jarjar-rules.txt
index e5d1a88..f658a5e 100644
--- a/service/jarjar-rules.txt
+++ b/service/jarjar-rules.txt
@@ -90,5 +90,12 @@
 # From services-connectivity-shared-srcs
 rule android.net.util.NetworkConstants* com.android.connectivity.@0
 
+# From modules-utils-statemachine
+rule com.android.internal.util.IState* com.android.connectivity.@0
+rule com.android.internal.util.State* com.android.connectivity.@0
+
+# From the API shims
+rule com.android.networkstack.apishim.** com.android.connectivity.@0
+
 # Remaining are connectivity sources in com.android.server and com.android.server.connectivity:
 # TODO: move to a subpackage of com.android.connectivity (such as com.android.connectivity.server)
diff --git a/service/jni/com_android_net_module_util/onload.cpp b/service/jni/com_android_net_module_util/onload.cpp
index 1d17622..07ae31c 100644
--- a/service/jni/com_android_net_module_util/onload.cpp
+++ b/service/jni/com_android_net_module_util/onload.cpp
@@ -20,6 +20,7 @@
 namespace android {
 
 int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name);
+int register_com_android_net_module_util_TcUtils(JNIEnv* env, char const* class_name);
 
 extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
     JNIEnv *env;
@@ -31,6 +32,9 @@
     if (register_com_android_net_module_util_BpfMap(env,
             "com/android/connectivity/com/android/net/module/util/BpfMap") < 0) return JNI_ERR;
 
+    if (register_com_android_net_module_util_TcUtils(env,
+            "com/android/connectivity/com/android/net/module/util/TcUtils") < 0) return JNI_ERR;
+
     return JNI_VERSION_1_6;
 }
 
diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
new file mode 100644
index 0000000..b445462
--- /dev/null
+++ b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
@@ -0,0 +1,527 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+#define LOG_TAG "jniClatCoordinator"
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/if_tun.h>
+#include <linux/ioctl.h>
+#include <log/log.h>
+#include <nativehelper/JNIHelp.h>
+#include <net/if.h>
+#include <spawn.h>
+#include <sys/wait.h>
+#include <string>
+
+#include <netjniutils/netjniutils.h>
+
+#include "libclat/bpfhelper.h"
+#include "libclat/clatutils.h"
+#include "nativehelper/scoped_utf_chars.h"
+
+// Sync from system/netd/include/netid_client.h
+#define MARK_UNSET 0u
+
+// Sync from system/netd/server/NetdConstants.h
+#define __INT_STRLEN(i) sizeof(#i)
+#define _INT_STRLEN(i) __INT_STRLEN(i)
+#define INT32_STRLEN _INT_STRLEN(INT32_MIN)
+
+#define DEVICEPREFIX "v4-"
+
+namespace android {
+static const char* kClatdPath = "/apex/com.android.tethering/bin/for-system/clatd";
+
+static void throwIOException(JNIEnv* env, const char* msg, int error) {
+    jniThrowExceptionFmt(env, "java/io/IOException", "%s: %s", msg, strerror(error));
+}
+
+jstring com_android_server_connectivity_ClatCoordinator_selectIpv4Address(JNIEnv* env,
+                                                                          jobject clazz,
+                                                                          jstring v4addr,
+                                                                          jint prefixlen) {
+    ScopedUtfChars address(env, v4addr);
+    in_addr ip;
+    if (inet_pton(AF_INET, address.c_str(), &ip) != 1) {
+        throwIOException(env, "invalid address", EINVAL);
+        return nullptr;
+    }
+
+    // Pick an IPv4 address.
+    // TODO: this picks the address based on other addresses that are assigned to interfaces, but
+    // the address is only actually assigned to an interface once clatd starts up. So we could end
+    // up with two clatd instances with the same IPv4 address.
+    // Stop doing this and instead pick a free one from the kV4Addr pool.
+    in_addr v4 = {net::clat::selectIpv4Address(ip, prefixlen)};
+    if (v4.s_addr == INADDR_NONE) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "No free IPv4 address in %s/%d",
+                             address.c_str(), prefixlen);
+        return nullptr;
+    }
+
+    char addrstr[INET_ADDRSTRLEN];
+    if (!inet_ntop(AF_INET, (void*)&v4, addrstr, sizeof(addrstr))) {
+        throwIOException(env, "invalid address", EADDRNOTAVAIL);
+        return nullptr;
+    }
+    return env->NewStringUTF(addrstr);
+}
+
+// Picks a random interface ID that is checksum neutral with the IPv4 address and the NAT64 prefix.
+jstring com_android_server_connectivity_ClatCoordinator_generateIpv6Address(
+        JNIEnv* env, jobject clazz, jstring ifaceStr, jstring v4Str, jstring prefix64Str) {
+    ScopedUtfChars iface(env, ifaceStr);
+    ScopedUtfChars addr4(env, v4Str);
+    ScopedUtfChars prefix64(env, prefix64Str);
+
+    if (iface.c_str() == nullptr) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid null interface name");
+        return nullptr;
+    }
+
+    in_addr v4;
+    if (inet_pton(AF_INET, addr4.c_str(), &v4) != 1) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid clat v4 address %s",
+                             addr4.c_str());
+        return nullptr;
+    }
+
+    in6_addr nat64Prefix;
+    if (inet_pton(AF_INET6, prefix64.c_str(), &nat64Prefix) != 1) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid prefix %s", prefix64.c_str());
+        return nullptr;
+    }
+
+    in6_addr v6;
+    if (net::clat::generateIpv6Address(iface.c_str(), v4, nat64Prefix, &v6)) {
+        jniThrowExceptionFmt(env, "java/io/IOException",
+                             "Unable to find global source address on %s for %s", iface.c_str(),
+                             prefix64.c_str());
+        return nullptr;
+    }
+
+    char addrstr[INET6_ADDRSTRLEN];
+    if (!inet_ntop(AF_INET6, (void*)&v6, addrstr, sizeof(addrstr))) {
+        throwIOException(env, "invalid address", EADDRNOTAVAIL);
+        return nullptr;
+    }
+    return env->NewStringUTF(addrstr);
+}
+
+static jint com_android_server_connectivity_ClatCoordinator_createTunInterface(JNIEnv* env,
+                                                                               jobject clazz,
+                                                                               jstring tuniface) {
+    ScopedUtfChars v4interface(env, tuniface);
+
+    // open the tun device in non blocking mode as required by clatd
+    jint fd = open("/dev/net/tun", O_RDWR | O_NONBLOCK | O_CLOEXEC);
+    if (fd == -1) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "open tun device failed (%s)",
+                             strerror(errno));
+        return -1;
+    }
+
+    struct ifreq ifr = {
+            .ifr_flags = IFF_TUN,
+    };
+    strlcpy(ifr.ifr_name, v4interface.c_str(), sizeof(ifr.ifr_name));
+
+    if (ioctl(fd, TUNSETIFF, &ifr, sizeof(ifr))) {
+        close(fd);
+        jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(TUNSETIFF) failed (%s)",
+                             strerror(errno));
+        return -1;
+    }
+
+    return fd;
+}
+
+static jint com_android_server_connectivity_ClatCoordinator_detectMtu(JNIEnv* env, jobject clazz,
+                                                                      jstring platSubnet,
+                                                                      jint plat_suffix, jint mark) {
+    ScopedUtfChars platSubnetStr(env, platSubnet);
+
+    in6_addr plat_subnet;
+    if (inet_pton(AF_INET6, platSubnetStr.c_str(), &plat_subnet) != 1) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid plat prefix address %s",
+                             platSubnetStr.c_str());
+        return -1;
+    }
+
+    int ret = net::clat::detect_mtu(&plat_subnet, plat_suffix, mark);
+    if (ret < 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "detect mtu failed: %s", strerror(-ret));
+        return -1;
+    }
+
+    return ret;
+}
+
+static jint com_android_server_connectivity_ClatCoordinator_openPacketSocket(JNIEnv* env,
+                                                                              jobject clazz) {
+    // Will eventually be bound to htons(ETH_P_IPV6) protocol,
+    // but only after appropriate bpf filter is attached.
+    int sock = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+    if (sock < 0) {
+        throwIOException(env, "packet socket failed", errno);
+        return -1;
+    }
+    return sock;
+}
+
+static jint com_android_server_connectivity_ClatCoordinator_openRawSocket6(JNIEnv* env,
+                                                                           jobject clazz,
+                                                                           jint mark) {
+    int sock = socket(AF_INET6, SOCK_RAW | SOCK_NONBLOCK | SOCK_CLOEXEC, IPPROTO_RAW);
+    if (sock < 0) {
+        throwIOException(env, "raw socket failed", errno);
+        return -1;
+    }
+
+    // TODO: check the mark validation
+    if (mark != MARK_UNSET && setsockopt(sock, SOL_SOCKET, SO_MARK, &mark, sizeof(mark)) < 0) {
+        throwIOException(env, "could not set mark on raw socket", errno);
+        close(sock);
+        return -1;
+    }
+
+    return sock;
+}
+
+static void com_android_server_connectivity_ClatCoordinator_addAnycastSetsockopt(
+        JNIEnv* env, jobject clazz, jobject javaFd, jstring addr6, jint ifindex) {
+    int sock = netjniutils::GetNativeFileDescriptor(env, javaFd);
+    if (sock < 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid file descriptor");
+        return;
+    }
+
+    ScopedUtfChars addrStr(env, addr6);
+
+    in6_addr addr;
+    if (inet_pton(AF_INET6, addrStr.c_str(), &addr) != 1) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid IPv6 address %s",
+                             addrStr.c_str());
+        return;
+    }
+
+    struct ipv6_mreq mreq = {addr, ifindex};
+    int ret = setsockopt(sock, SOL_IPV6, IPV6_JOIN_ANYCAST, &mreq, sizeof(mreq));
+    if (ret) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "setsockopt IPV6_JOIN_ANYCAST failed: %s",
+                             strerror(errno));
+        return;
+    }
+}
+
+static void com_android_server_connectivity_ClatCoordinator_configurePacketSocket(
+        JNIEnv* env, jobject clazz, jobject javaFd, jstring addr6, jint ifindex) {
+    ScopedUtfChars addrStr(env, addr6);
+
+    int sock = netjniutils::GetNativeFileDescriptor(env, javaFd);
+    if (sock < 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid file descriptor");
+        return;
+    }
+
+    in6_addr addr;
+    if (inet_pton(AF_INET6, addrStr.c_str(), &addr) != 1) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid IPv6 address %s",
+                             addrStr.c_str());
+        return;
+    }
+
+    int ret = net::clat::configure_packet_socket(sock, &addr, ifindex);
+    if (ret < 0) {
+        throwIOException(env, "configure packet socket failed", -ret);
+        return;
+    }
+}
+
+int initTracker(const std::string& iface, const std::string& pfx96, const std::string& v4,
+        const std::string& v6, net::clat::ClatdTracker* output) {
+    strlcpy(output->iface, iface.c_str(), sizeof(output->iface));
+    output->ifIndex = if_nametoindex(iface.c_str());
+    if (output->ifIndex == 0) {
+        ALOGE("interface %s not found", output->iface);
+        return -1;
+    }
+
+    unsigned len = snprintf(output->v4iface, sizeof(output->v4iface),
+            "%s%s", DEVICEPREFIX, iface.c_str());
+    if (len >= sizeof(output->v4iface)) {
+        ALOGE("interface name too long '%s'", output->v4iface);
+        return -1;
+    }
+
+    output->v4ifIndex = if_nametoindex(output->v4iface);
+    if (output->v4ifIndex == 0) {
+        ALOGE("v4-interface %s not found", output->v4iface);
+        return -1;
+    }
+
+    if (!inet_pton(AF_INET6, pfx96.c_str(), &output->pfx96)) {
+        ALOGE("invalid IPv6 address specified for plat prefix: %s", pfx96.c_str());
+        return -1;
+    }
+
+    if (!inet_pton(AF_INET, v4.c_str(), &output->v4)) {
+        ALOGE("Invalid IPv4 address %s", v4.c_str());
+        return -1;
+    }
+
+    if (!inet_pton(AF_INET6, v6.c_str(), &output->v6)) {
+        ALOGE("Invalid source address %s", v6.c_str());
+        return -1;
+    }
+
+    return 0;
+}
+
+static jint com_android_server_connectivity_ClatCoordinator_startClatd(
+        JNIEnv* env, jobject clazz, jobject tunJavaFd, jobject readSockJavaFd,
+        jobject writeSockJavaFd, jstring iface, jstring pfx96, jstring v4, jstring v6) {
+    ScopedUtfChars ifaceStr(env, iface);
+    ScopedUtfChars pfx96Str(env, pfx96);
+    ScopedUtfChars v4Str(env, v4);
+    ScopedUtfChars v6Str(env, v6);
+
+    int tunFd = netjniutils::GetNativeFileDescriptor(env, tunJavaFd);
+    if (tunFd < 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid tun file descriptor");
+        return -1;
+    }
+
+    int readSock = netjniutils::GetNativeFileDescriptor(env, readSockJavaFd);
+    if (readSock < 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid read socket");
+        return -1;
+    }
+
+    int writeSock = netjniutils::GetNativeFileDescriptor(env, writeSockJavaFd);
+    if (writeSock < 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid write socket");
+        return -1;
+    }
+
+    // 1. create a throwaway socket to reserve a file descriptor number
+    int passedTunFd = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+    if (passedTunFd == -1) {
+        throwIOException(env, "socket(ipv6/udp) for tun fd failed", errno);
+        return -1;
+    }
+    int passedSockRead = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+    if (passedSockRead == -1) {
+        throwIOException(env, "socket(ipv6/udp) for read socket failed", errno);
+        return -1;
+    }
+    int passedSockWrite = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+    if (passedSockWrite == -1) {
+        throwIOException(env, "socket(ipv6/udp) for write socket failed", errno);
+        return -1;
+    }
+
+    // these are the FD we'll pass to clatd on the cli, so need it as a string
+    char passedTunFdStr[INT32_STRLEN];
+    char passedSockReadStr[INT32_STRLEN];
+    char passedSockWriteStr[INT32_STRLEN];
+    snprintf(passedTunFdStr, sizeof(passedTunFdStr), "%d", passedTunFd);
+    snprintf(passedSockReadStr, sizeof(passedSockReadStr), "%d", passedSockRead);
+    snprintf(passedSockWriteStr, sizeof(passedSockWriteStr), "%d", passedSockWrite);
+
+    // 2. we're going to use this as argv[0] to clatd to make ps output more useful
+    std::string progname("clatd-");
+    progname += ifaceStr.c_str();
+
+    // clang-format off
+    const char* args[] = {progname.c_str(),
+                          "-i", ifaceStr.c_str(),
+                          "-p", pfx96Str.c_str(),
+                          "-4", v4Str.c_str(),
+                          "-6", v6Str.c_str(),
+                          "-t", passedTunFdStr,
+                          "-r", passedSockReadStr,
+                          "-w", passedSockWriteStr,
+                          nullptr};
+    // clang-format on
+
+    // 3. register vfork requirement
+    posix_spawnattr_t attr;
+    if (int ret = posix_spawnattr_init(&attr)) {
+        throwIOException(env, "posix_spawnattr_init failed", ret);
+        return -1;
+    }
+
+    // TODO: use android::base::ScopeGuard.
+    if (int ret = posix_spawnattr_setflags(&attr, POSIX_SPAWN_USEVFORK)) {
+        posix_spawnattr_destroy(&attr);
+        throwIOException(env, "posix_spawnattr_setflags failed", ret);
+        return -1;
+    }
+
+    // 4. register dup2() action: this is what 'clears' the CLOEXEC flag
+    // on the tun fd that we want the child clatd process to inherit
+    // (this will happen after the vfork, and before the execve)
+    posix_spawn_file_actions_t fa;
+    if (int ret = posix_spawn_file_actions_init(&fa)) {
+        posix_spawnattr_destroy(&attr);
+        throwIOException(env, "posix_spawn_file_actions_init failed", ret);
+        return -1;
+    }
+
+    if (int ret = posix_spawn_file_actions_adddup2(&fa, tunFd, passedTunFd)) {
+        posix_spawnattr_destroy(&attr);
+        posix_spawn_file_actions_destroy(&fa);
+        throwIOException(env, "posix_spawn_file_actions_adddup2 for tun fd failed", ret);
+        return -1;
+    }
+    if (int ret = posix_spawn_file_actions_adddup2(&fa, readSock, passedSockRead)) {
+        posix_spawnattr_destroy(&attr);
+        posix_spawn_file_actions_destroy(&fa);
+        throwIOException(env, "posix_spawn_file_actions_adddup2 for read socket failed", ret);
+        return -1;
+    }
+    if (int ret = posix_spawn_file_actions_adddup2(&fa, writeSock, passedSockWrite)) {
+        posix_spawnattr_destroy(&attr);
+        posix_spawn_file_actions_destroy(&fa);
+        throwIOException(env, "posix_spawn_file_actions_adddup2 for write socket failed", ret);
+        return -1;
+    }
+
+    // 5. actually perform vfork/dup2/execve
+    pid_t pid;
+    if (int ret = posix_spawn(&pid, kClatdPath, &fa, &attr, (char* const*)args, nullptr)) {
+        posix_spawnattr_destroy(&attr);
+        posix_spawn_file_actions_destroy(&fa);
+        throwIOException(env, "posix_spawn failed", ret);
+        return -1;
+    }
+
+    posix_spawnattr_destroy(&attr);
+    posix_spawn_file_actions_destroy(&fa);
+
+    // 5. Start BPF if any
+    if (!net::clat::initMaps()) {
+        net::clat::ClatdTracker tracker = {};
+        if (!initTracker(ifaceStr.c_str(), pfx96Str.c_str(), v4Str.c_str(), v6Str.c_str(),
+                &tracker)) {
+            net::clat::maybeStartBpf(tracker);
+        }
+    }
+
+    return pid;
+}
+
+// Stop clatd process. SIGTERM with timeout first, if fail, SIGKILL.
+// See stopProcess() in system/netd/server/NetdConstants.cpp.
+// TODO: have a function stopProcess(int pid, const char *name) in common location and call it.
+static constexpr int WAITPID_ATTEMPTS = 50;
+static constexpr int WAITPID_RETRY_INTERVAL_US = 100000;
+
+static void stopClatdProcess(int pid) {
+    int err = kill(pid, SIGTERM);
+    if (err) {
+        err = errno;
+    }
+    if (err == ESRCH) {
+        ALOGE("clatd child process %d unexpectedly disappeared", pid);
+        return;
+    }
+    if (err) {
+        ALOGE("Error killing clatd child process %d: %s", pid, strerror(err));
+    }
+    int status = 0;
+    int ret = 0;
+    for (int count = 0; ret == 0 && count < WAITPID_ATTEMPTS; count++) {
+        usleep(WAITPID_RETRY_INTERVAL_US);
+        ret = waitpid(pid, &status, WNOHANG);
+    }
+    if (ret == 0) {
+        ALOGE("Failed to SIGTERM clatd pid=%d, try SIGKILL", pid);
+        // TODO: fix that kill failed or waitpid doesn't return.
+        kill(pid, SIGKILL);
+        ret = waitpid(pid, &status, 0);
+    }
+    if (ret == -1) {
+        ALOGE("Error waiting for clatd child process %d: %s", pid, strerror(errno));
+    } else {
+        ALOGD("clatd process %d terminated status=%d", pid, status);
+    }
+}
+
+static void com_android_server_connectivity_ClatCoordinator_stopClatd(JNIEnv* env, jobject clazz,
+                                                                      jstring iface, jstring pfx96,
+                                                                      jstring v4, jstring v6,
+                                                                      jint pid) {
+    ScopedUtfChars ifaceStr(env, iface);
+    ScopedUtfChars pfx96Str(env, pfx96);
+    ScopedUtfChars v4Str(env, v4);
+    ScopedUtfChars v6Str(env, v6);
+
+    if (pid <= 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid pid");
+        return;
+    }
+
+    if (!net::clat::initMaps()) {
+        net::clat::ClatdTracker tracker = {};
+        if (!initTracker(ifaceStr.c_str(), pfx96Str.c_str(), v4Str.c_str(), v6Str.c_str(),
+                &tracker)) {
+            net::clat::maybeStopBpf(tracker);
+        }
+    }
+
+    stopClatdProcess(pid);
+}
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+        /* name, signature, funcPtr */
+        {"native_selectIpv4Address", "(Ljava/lang/String;I)Ljava/lang/String;",
+         (void*)com_android_server_connectivity_ClatCoordinator_selectIpv4Address},
+        {"native_generateIpv6Address",
+         "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",
+         (void*)com_android_server_connectivity_ClatCoordinator_generateIpv6Address},
+        {"native_createTunInterface", "(Ljava/lang/String;)I",
+         (void*)com_android_server_connectivity_ClatCoordinator_createTunInterface},
+        {"native_detectMtu", "(Ljava/lang/String;II)I",
+         (void*)com_android_server_connectivity_ClatCoordinator_detectMtu},
+        {"native_openPacketSocket", "()I",
+         (void*)com_android_server_connectivity_ClatCoordinator_openPacketSocket},
+        {"native_openRawSocket6", "(I)I",
+         (void*)com_android_server_connectivity_ClatCoordinator_openRawSocket6},
+        {"native_addAnycastSetsockopt", "(Ljava/io/FileDescriptor;Ljava/lang/String;I)V",
+         (void*)com_android_server_connectivity_ClatCoordinator_addAnycastSetsockopt},
+        {"native_configurePacketSocket", "(Ljava/io/FileDescriptor;Ljava/lang/String;I)V",
+         (void*)com_android_server_connectivity_ClatCoordinator_configurePacketSocket},
+        {"native_startClatd",
+         "(Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;Ljava/lang/"
+         "String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I",
+         (void*)com_android_server_connectivity_ClatCoordinator_startClatd},
+        {"native_stopClatd",
+         "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V",
+         (void*)com_android_server_connectivity_ClatCoordinator_stopClatd},
+};
+
+int register_android_server_connectivity_ClatCoordinator(JNIEnv* env) {
+    return jniRegisterNativeMethods(env, "com/android/server/connectivity/ClatCoordinator",
+                                    gMethods, NELEM(gMethods));
+}
+
+};  // namespace android
diff --git a/service/jni/onload.cpp b/service/jni/onload.cpp
index 0012879..04d9671 100644
--- a/service/jni/onload.cpp
+++ b/service/jni/onload.cpp
@@ -20,6 +20,7 @@
 namespace android {
 
 int register_android_server_TestNetworkService(JNIEnv* env);
+int register_android_server_connectivity_ClatCoordinator(JNIEnv* env);
 
 extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
     JNIEnv *env;
@@ -32,6 +33,10 @@
         return JNI_ERR;
     }
 
+    if (register_android_server_connectivity_ClatCoordinator(env) < 0) {
+        return JNI_ERR;
+    }
+
     return JNI_VERSION_1_6;
 }
 
diff --git a/service/native/Android.bp b/service/native/Android.bp
new file mode 100644
index 0000000..b49457d
--- /dev/null
+++ b/service/native/Android.bp
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2022 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library {
+    name: "libtraffic_controller",
+    defaults: ["netd_defaults"],
+    srcs: [
+        "TrafficController.cpp",
+    ],
+    header_libs: [
+        "bpf_connectivity_headers",
+        "bpf_headers",
+        "bpf_syscall_wrappers",
+    ],
+    static_libs: [
+        "libnetdutils",
+        // TrafficController would use the constants of INetd so that add
+        // netd_aidl_interface-lateststable-ndk.
+        "netd_aidl_interface-lateststable-ndk",
+    ],
+    shared_libs: [
+        // TODO: Find a good way to remove libbase.
+        "libbase",
+        "libcutils",
+        "libutils",
+        "liblog",
+    ],
+    export_include_dirs: ["include"],
+    sanitize: {
+        cfi: true,
+    },
+    apex_available: [
+        "com.android.tethering",
+    ],
+    min_sdk_version: "30",
+}
+
+cc_library_shared {
+    name: "libtraffic_controller_jni",
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+        "-Wthread-safety",
+    ],
+    srcs: [
+        "jni/*.cpp",
+    ],
+    header_libs: [
+        "bpf_connectivity_headers",
+    ],
+    static_libs: [
+        "libnetdutils",
+        "libtraffic_controller",
+        "netd_aidl_interface-lateststable-ndk",
+    ],
+    shared_libs: [
+        "libbase",
+        "liblog",
+        "libutils",
+        "libnativehelper",
+    ],
+    apex_available: [
+        "com.android.tethering",
+    ],
+    min_sdk_version: "30",
+}
+
+cc_test {
+    name: "traffic_controller_unit_test",
+    test_suites: ["general-tests"],
+    require_root: true,
+    local_include_dirs: ["include"],
+    header_libs: [
+        "bpf_connectivity_headers",
+    ],
+    srcs: [
+        "TrafficControllerTest.cpp",
+    ],
+    static_libs: [
+        "libbase",
+        "libgmock",
+        "liblog",
+        "libnetdutils",
+        "libtraffic_controller",
+        "libutils",
+        "netd_aidl_interface-lateststable-ndk",
+    ],
+}
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp
new file mode 100644
index 0000000..97fcc43
--- /dev/null
+++ b/service/native/TrafficController.cpp
@@ -0,0 +1,904 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+#define LOG_TAG "TrafficController"
+#include <inttypes.h>
+#include <linux/if_ether.h>
+#include <linux/in.h>
+#include <linux/inet_diag.h>
+#include <linux/netlink.h>
+#include <linux/sock_diag.h>
+#include <linux/unistd.h>
+#include <net/if.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/utsname.h>
+#include <sys/wait.h>
+#include <map>
+#include <mutex>
+#include <unordered_set>
+#include <vector>
+
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+#include <android-base/unique_fd.h>
+#include <netdutils/StatusOr.h>
+#include <netdutils/Syscalls.h>
+#include <netdutils/UidConstants.h>
+#include <netdutils/Utils.h>
+#include <private/android_filesystem_config.h>
+
+#include "TrafficController.h"
+#include "bpf/BpfMap.h"
+#include "netdutils/DumpWriter.h"
+
+namespace android {
+namespace net {
+
+using base::StringPrintf;
+using base::unique_fd;
+using bpf::BpfMap;
+using bpf::OVERFLOW_COUNTERSET;
+using bpf::synchronizeKernelRCU;
+using netdutils::DumpWriter;
+using netdutils::getIfaceList;
+using netdutils::NetlinkListener;
+using netdutils::NetlinkListenerInterface;
+using netdutils::ScopedIndent;
+using netdutils::Slice;
+using netdutils::sSyscalls;
+using netdutils::Status;
+using netdutils::statusFromErrno;
+using netdutils::StatusOr;
+using netdutils::status::ok;
+
+constexpr int kSockDiagMsgType = SOCK_DIAG_BY_FAMILY;
+constexpr int kSockDiagDoneMsgType = NLMSG_DONE;
+
+const char* TrafficController::LOCAL_DOZABLE = "fw_dozable";
+const char* TrafficController::LOCAL_STANDBY = "fw_standby";
+const char* TrafficController::LOCAL_POWERSAVE = "fw_powersave";
+const char* TrafficController::LOCAL_RESTRICTED = "fw_restricted";
+
+static_assert(BPF_PERMISSION_INTERNET == INetd::PERMISSION_INTERNET,
+              "Mismatch between BPF and AIDL permissions: PERMISSION_INTERNET");
+static_assert(BPF_PERMISSION_UPDATE_DEVICE_STATS == INetd::PERMISSION_UPDATE_DEVICE_STATS,
+              "Mismatch between BPF and AIDL permissions: PERMISSION_UPDATE_DEVICE_STATS");
+
+#define FLAG_MSG_TRANS(result, flag, value) \
+    do {                                    \
+        if ((value) & (flag)) {             \
+            (result).append(" " #flag);     \
+            (value) &= ~(flag);             \
+        }                                   \
+    } while (0)
+
+const std::string uidMatchTypeToString(uint8_t match) {
+    std::string matchType;
+    FLAG_MSG_TRANS(matchType, HAPPY_BOX_MATCH, match);
+    FLAG_MSG_TRANS(matchType, PENALTY_BOX_MATCH, match);
+    FLAG_MSG_TRANS(matchType, DOZABLE_MATCH, match);
+    FLAG_MSG_TRANS(matchType, STANDBY_MATCH, match);
+    FLAG_MSG_TRANS(matchType, POWERSAVE_MATCH, match);
+    FLAG_MSG_TRANS(matchType, RESTRICTED_MATCH, match);
+    FLAG_MSG_TRANS(matchType, IIF_MATCH, match);
+    if (match) {
+        return StringPrintf("Unknown match: %u", match);
+    }
+    return matchType;
+}
+
+bool TrafficController::hasUpdateDeviceStatsPermission(uid_t uid) {
+    // This implementation is the same logic as method ActivityManager#checkComponentPermission.
+    // It implies that the calling uid can never be the same as PER_USER_RANGE.
+    uint32_t appId = uid % PER_USER_RANGE;
+    return ((appId == AID_ROOT) || (appId == AID_SYSTEM) ||
+            mPrivilegedUser.find(appId) != mPrivilegedUser.end());
+}
+
+const std::string UidPermissionTypeToString(int permission) {
+    if (permission == INetd::PERMISSION_NONE) {
+        return "PERMISSION_NONE";
+    }
+    if (permission == INetd::PERMISSION_UNINSTALLED) {
+        // This should never appear in the map, complain loudly if it does.
+        return "PERMISSION_UNINSTALLED error!";
+    }
+    std::string permissionType;
+    FLAG_MSG_TRANS(permissionType, BPF_PERMISSION_INTERNET, permission);
+    FLAG_MSG_TRANS(permissionType, BPF_PERMISSION_UPDATE_DEVICE_STATS, permission);
+    if (permission) {
+        return StringPrintf("Unknown permission: %u", permission);
+    }
+    return permissionType;
+}
+
+StatusOr<std::unique_ptr<NetlinkListenerInterface>> TrafficController::makeSkDestroyListener() {
+    const auto& sys = sSyscalls.get();
+    ASSIGN_OR_RETURN(auto event, sys.eventfd(0, EFD_CLOEXEC));
+    const int domain = AF_NETLINK;
+    const int type = SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK;
+    const int protocol = NETLINK_INET_DIAG;
+    ASSIGN_OR_RETURN(auto sock, sys.socket(domain, type, protocol));
+
+    // TODO: if too many sockets are closed too quickly, we can overflow the socket buffer, and
+    // some entries in mCookieTagMap will not be freed. In order to fix this we would need to
+    // periodically dump all sockets and remove the tag entries for sockets that have been closed.
+    // For now, set a large-enough buffer that we can close hundreds of sockets without getting
+    // ENOBUFS and leaking mCookieTagMap entries.
+    int rcvbuf = 512 * 1024;
+    auto ret = sys.setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
+    if (!ret.ok()) {
+        ALOGW("Failed to set SkDestroyListener buffer size to %d: %s", rcvbuf, ret.msg().c_str());
+    }
+
+    sockaddr_nl addr = {
+        .nl_family = AF_NETLINK,
+        .nl_groups = 1 << (SKNLGRP_INET_TCP_DESTROY - 1) | 1 << (SKNLGRP_INET_UDP_DESTROY - 1) |
+                     1 << (SKNLGRP_INET6_TCP_DESTROY - 1) | 1 << (SKNLGRP_INET6_UDP_DESTROY - 1)};
+    RETURN_IF_NOT_OK(sys.bind(sock, addr));
+
+    const sockaddr_nl kernel = {.nl_family = AF_NETLINK};
+    RETURN_IF_NOT_OK(sys.connect(sock, kernel));
+
+    std::unique_ptr<NetlinkListenerInterface> listener =
+            std::make_unique<NetlinkListener>(std::move(event), std::move(sock), "SkDestroyListen");
+
+    return listener;
+}
+
+Status TrafficController::initMaps() {
+    std::lock_guard guard(mMutex);
+
+    RETURN_IF_NOT_OK(mCookieTagMap.init(COOKIE_TAG_MAP_PATH));
+    RETURN_IF_NOT_OK(mUidCounterSetMap.init(UID_COUNTERSET_MAP_PATH));
+    RETURN_IF_NOT_OK(mAppUidStatsMap.init(APP_UID_STATS_MAP_PATH));
+    RETURN_IF_NOT_OK(mStatsMapA.init(STATS_MAP_A_PATH));
+    RETURN_IF_NOT_OK(mStatsMapB.init(STATS_MAP_B_PATH));
+    RETURN_IF_NOT_OK(mIfaceIndexNameMap.init(IFACE_INDEX_NAME_MAP_PATH));
+    RETURN_IF_NOT_OK(mIfaceStatsMap.init(IFACE_STATS_MAP_PATH));
+
+    RETURN_IF_NOT_OK(mConfigurationMap.init(CONFIGURATION_MAP_PATH));
+    RETURN_IF_NOT_OK(
+            mConfigurationMap.writeValue(UID_RULES_CONFIGURATION_KEY, DEFAULT_CONFIG, BPF_ANY));
+    RETURN_IF_NOT_OK(mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY, SELECT_MAP_A,
+                                                  BPF_ANY));
+
+    RETURN_IF_NOT_OK(mUidOwnerMap.init(UID_OWNER_MAP_PATH));
+    RETURN_IF_NOT_OK(mUidOwnerMap.clear());
+    RETURN_IF_NOT_OK(mUidPermissionMap.init(UID_PERMISSION_MAP_PATH));
+
+    return netdutils::status::ok;
+}
+
+Status TrafficController::start() {
+    RETURN_IF_NOT_OK(initMaps());
+
+    // Fetch the list of currently-existing interfaces. At this point NetlinkHandler is
+    // already running, so it will call addInterface() when any new interface appears.
+    // TODO: Clean-up addInterface() after interface monitoring is in
+    // NetworkStatsService.
+    std::map<std::string, uint32_t> ifacePairs;
+    ASSIGN_OR_RETURN(ifacePairs, getIfaceList());
+    for (const auto& ifacePair:ifacePairs) {
+        addInterface(ifacePair.first.c_str(), ifacePair.second);
+    }
+
+    auto result = makeSkDestroyListener();
+    if (!isOk(result)) {
+        ALOGE("Unable to create SkDestroyListener: %s", toString(result).c_str());
+    } else {
+        mSkDestroyListener = std::move(result.value());
+    }
+    // Rx handler extracts nfgenmsg looks up and invokes registered dispatch function.
+    const auto rxHandler = [this](const nlmsghdr&, const Slice msg) {
+        std::lock_guard guard(mMutex);
+        inet_diag_msg diagmsg = {};
+        if (extract(msg, diagmsg) < sizeof(inet_diag_msg)) {
+            ALOGE("Unrecognized netlink message: %s", toString(msg).c_str());
+            return;
+        }
+        uint64_t sock_cookie = static_cast<uint64_t>(diagmsg.id.idiag_cookie[0]) |
+                               (static_cast<uint64_t>(diagmsg.id.idiag_cookie[1]) << 32);
+
+        Status s = mCookieTagMap.deleteValue(sock_cookie);
+        if (!isOk(s) && s.code() != ENOENT) {
+            ALOGE("Failed to delete cookie %" PRIx64 ": %s", sock_cookie, toString(s).c_str());
+            return;
+        }
+    };
+    expectOk(mSkDestroyListener->subscribe(kSockDiagMsgType, rxHandler));
+
+    // In case multiple netlink message comes in as a stream, we need to handle the rxDone message
+    // properly.
+    const auto rxDoneHandler = [](const nlmsghdr&, const Slice msg) {
+        // Ignore NLMSG_DONE  messages
+        inet_diag_msg diagmsg = {};
+        extract(msg, diagmsg);
+    };
+    expectOk(mSkDestroyListener->subscribe(kSockDiagDoneMsgType, rxDoneHandler));
+
+    return netdutils::status::ok;
+}
+
+int TrafficController::setCounterSet(int counterSetNum, uid_t uid, uid_t callingUid) {
+    if (counterSetNum < 0 || counterSetNum >= OVERFLOW_COUNTERSET) return -EINVAL;
+
+    std::lock_guard guard(mMutex);
+    if (!hasUpdateDeviceStatsPermission(callingUid)) return -EPERM;
+
+    // The default counter set for all uid is 0, so deleting the current counterset for that uid
+    // will automatically set it to 0.
+    if (counterSetNum == 0) {
+        Status res = mUidCounterSetMap.deleteValue(uid);
+        if (isOk(res) || (!isOk(res) && res.code() == ENOENT)) {
+            return 0;
+        } else {
+            ALOGE("Failed to delete the counterSet: %s\n", strerror(res.code()));
+            return -res.code();
+        }
+    }
+    uint8_t tmpCounterSetNum = (uint8_t)counterSetNum;
+    Status res = mUidCounterSetMap.writeValue(uid, tmpCounterSetNum, BPF_ANY);
+    if (!isOk(res)) {
+        ALOGE("Failed to set the counterSet: %s, fd: %d", strerror(res.code()),
+              mUidCounterSetMap.getMap().get());
+        return -res.code();
+    }
+    return 0;
+}
+
+// This method only get called by system_server when an app get uinstalled, it
+// is called inside removeUidsLocked() while holding mStatsLock. So it is safe
+// to iterate and modify the stats maps.
+int TrafficController::deleteTagData(uint32_t tag, uid_t uid, uid_t callingUid) {
+    std::lock_guard guard(mMutex);
+    if (!hasUpdateDeviceStatsPermission(callingUid)) return -EPERM;
+
+    // First we go through the cookieTagMap to delete the target uid tag combination. Or delete all
+    // the tags related to the uid if the tag is 0.
+    const auto deleteMatchedCookieEntries = [uid, tag](const uint64_t& key,
+                                                       const UidTagValue& value,
+                                                       BpfMap<uint64_t, UidTagValue>& map) {
+        if (value.uid == uid && (value.tag == tag || tag == 0)) {
+            auto res = map.deleteValue(key);
+            if (res.ok() || (res.error().code() == ENOENT)) {
+                return base::Result<void>();
+            }
+            ALOGE("Failed to delete data(cookie = %" PRIu64 "): %s\n", key,
+                  strerror(res.error().code()));
+        }
+        // Move forward to next cookie in the map.
+        return base::Result<void>();
+    };
+    mCookieTagMap.iterateWithValue(deleteMatchedCookieEntries);
+    // Now we go through the Tag stats map and delete the data entry with correct uid and tag
+    // combination. Or all tag stats under that uid if the target tag is 0.
+    const auto deleteMatchedUidTagEntries = [uid, tag](const StatsKey& key,
+                                                       BpfMap<StatsKey, StatsValue>& map) {
+        if (key.uid == uid && (key.tag == tag || tag == 0)) {
+            auto res = map.deleteValue(key);
+            if (res.ok() || (res.error().code() == ENOENT)) {
+                //Entry is deleted, use the current key to get a new nextKey;
+                return base::Result<void>();
+            }
+            ALOGE("Failed to delete data(uid=%u, tag=%u): %s\n", key.uid, key.tag,
+                  strerror(res.error().code()));
+        }
+        return base::Result<void>();
+    };
+    mStatsMapB.iterate(deleteMatchedUidTagEntries);
+    mStatsMapA.iterate(deleteMatchedUidTagEntries);
+    // If the tag is not zero, we already deleted all the data entry required. If tag is 0, we also
+    // need to delete the stats stored in uidStatsMap and counterSet map.
+    if (tag != 0) return 0;
+
+    auto res = mUidCounterSetMap.deleteValue(uid);
+    if (!res.ok() && res.error().code() != ENOENT) {
+        ALOGE("Failed to delete counterSet data(uid=%u, tag=%u): %s\n", uid, tag,
+              strerror(res.error().code()));
+    }
+
+    auto deleteAppUidStatsEntry = [uid](const uint32_t& key,
+                                        BpfMap<uint32_t, StatsValue>& map) -> base::Result<void> {
+        if (key == uid) {
+            auto res = map.deleteValue(key);
+            if (res.ok() || (res.error().code() == ENOENT)) {
+                return {};
+            }
+            ALOGE("Failed to delete data(uid=%u): %s", key, strerror(res.error().code()));
+        }
+        return {};
+    };
+    mAppUidStatsMap.iterate(deleteAppUidStatsEntry);
+    return 0;
+}
+
+int TrafficController::addInterface(const char* name, uint32_t ifaceIndex) {
+    IfaceValue iface;
+    if (ifaceIndex == 0) {
+        ALOGE("Unknown interface %s(%d)", name, ifaceIndex);
+        return -1;
+    }
+
+    strlcpy(iface.name, name, sizeof(IfaceValue));
+    Status res = mIfaceIndexNameMap.writeValue(ifaceIndex, iface, BPF_ANY);
+    if (!isOk(res)) {
+        ALOGE("Failed to add iface %s(%d): %s", name, ifaceIndex, strerror(res.code()));
+        return -res.code();
+    }
+    return 0;
+}
+
+Status TrafficController::updateOwnerMapEntry(UidOwnerMatchType match, uid_t uid, FirewallRule rule,
+                                              FirewallType type) {
+    std::lock_guard guard(mMutex);
+    if ((rule == ALLOW && type == ALLOWLIST) || (rule == DENY && type == DENYLIST)) {
+        RETURN_IF_NOT_OK(addRule(uid, match));
+    } else if ((rule == ALLOW && type == DENYLIST) || (rule == DENY && type == ALLOWLIST)) {
+        RETURN_IF_NOT_OK(removeRule(uid, match));
+    } else {
+        //Cannot happen.
+        return statusFromErrno(EINVAL, "");
+    }
+    return netdutils::status::ok;
+}
+
+Status TrafficController::removeRule(uint32_t uid, UidOwnerMatchType match) {
+    auto oldMatch = mUidOwnerMap.readValue(uid);
+    if (oldMatch.ok()) {
+        UidOwnerValue newMatch = {
+                .iif = (match == IIF_MATCH) ? 0 : oldMatch.value().iif,
+                .rule = static_cast<uint8_t>(oldMatch.value().rule & ~match),
+        };
+        if (newMatch.rule == 0) {
+            RETURN_IF_NOT_OK(mUidOwnerMap.deleteValue(uid));
+        } else {
+            RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
+        }
+    } else {
+        return statusFromErrno(ENOENT, StringPrintf("uid: %u does not exist in map", uid));
+    }
+    return netdutils::status::ok;
+}
+
+Status TrafficController::addRule(uint32_t uid, UidOwnerMatchType match, uint32_t iif) {
+    // iif should be non-zero if and only if match == MATCH_IIF
+    if (match == IIF_MATCH && iif == 0) {
+        return statusFromErrno(EINVAL, "Interface match must have nonzero interface index");
+    } else if (match != IIF_MATCH && iif != 0) {
+        return statusFromErrno(EINVAL, "Non-interface match must have zero interface index");
+    }
+    auto oldMatch = mUidOwnerMap.readValue(uid);
+    if (oldMatch.ok()) {
+        UidOwnerValue newMatch = {
+                .iif = iif ? iif : oldMatch.value().iif,
+                .rule = static_cast<uint8_t>(oldMatch.value().rule | match),
+        };
+        RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
+    } else {
+        UidOwnerValue newMatch = {
+                .iif = iif,
+                .rule = static_cast<uint8_t>(match),
+        };
+        RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
+    }
+    return netdutils::status::ok;
+}
+
+Status TrafficController::updateUidOwnerMap(const uint32_t uid,
+                                            UidOwnerMatchType matchType, IptOp op) {
+    std::lock_guard guard(mMutex);
+    if (op == IptOpDelete) {
+        RETURN_IF_NOT_OK(removeRule(uid, matchType));
+    } else if (op == IptOpInsert) {
+        RETURN_IF_NOT_OK(addRule(uid, matchType));
+    } else {
+        // Cannot happen.
+        return statusFromErrno(EINVAL, StringPrintf("invalid IptOp: %d, %d", op, matchType));
+    }
+    return netdutils::status::ok;
+}
+
+FirewallType TrafficController::getFirewallType(ChildChain chain) {
+    switch (chain) {
+        case DOZABLE:
+            return ALLOWLIST;
+        case STANDBY:
+            return DENYLIST;
+        case POWERSAVE:
+            return ALLOWLIST;
+        case RESTRICTED:
+            return ALLOWLIST;
+        case NONE:
+        default:
+            return DENYLIST;
+    }
+}
+
+int TrafficController::changeUidOwnerRule(ChildChain chain, uid_t uid, FirewallRule rule,
+                                          FirewallType type) {
+    Status res;
+    switch (chain) {
+        case DOZABLE:
+            res = updateOwnerMapEntry(DOZABLE_MATCH, uid, rule, type);
+            break;
+        case STANDBY:
+            res = updateOwnerMapEntry(STANDBY_MATCH, uid, rule, type);
+            break;
+        case POWERSAVE:
+            res = updateOwnerMapEntry(POWERSAVE_MATCH, uid, rule, type);
+            break;
+        case RESTRICTED:
+            res = updateOwnerMapEntry(RESTRICTED_MATCH, uid, rule, type);
+            break;
+        case NONE:
+        default:
+            ALOGW("Unknown child chain: %d", chain);
+            return -EINVAL;
+    }
+    if (!isOk(res)) {
+        ALOGE("change uid(%u) rule of %d failed: %s, rule: %d, type: %d", uid, chain,
+              res.msg().c_str(), rule, type);
+        return -res.code();
+    }
+    return 0;
+}
+
+Status TrafficController::replaceRulesInMap(const UidOwnerMatchType match,
+                                            const std::vector<int32_t>& uids) {
+    std::lock_guard guard(mMutex);
+    std::set<int32_t> uidSet(uids.begin(), uids.end());
+    std::vector<uint32_t> uidsToDelete;
+    auto getUidsToDelete = [&uidsToDelete, &uidSet](const uint32_t& key,
+                                                    const BpfMap<uint32_t, UidOwnerValue>&) {
+        if (uidSet.find((int32_t) key) == uidSet.end()) {
+            uidsToDelete.push_back(key);
+        }
+        return base::Result<void>();
+    };
+    RETURN_IF_NOT_OK(mUidOwnerMap.iterate(getUidsToDelete));
+
+    for(auto uid : uidsToDelete) {
+        RETURN_IF_NOT_OK(removeRule(uid, match));
+    }
+
+    for (auto uid : uids) {
+        RETURN_IF_NOT_OK(addRule(uid, match));
+    }
+    return netdutils::status::ok;
+}
+
+Status TrafficController::addUidInterfaceRules(const int iif,
+                                               const std::vector<int32_t>& uidsToAdd) {
+    if (!iif) {
+        return statusFromErrno(EINVAL, "Interface rule must specify interface");
+    }
+    std::lock_guard guard(mMutex);
+
+    for (auto uid : uidsToAdd) {
+        netdutils::Status result = addRule(uid, IIF_MATCH, iif);
+        if (!isOk(result)) {
+            ALOGW("addRule failed(%d): uid=%d iif=%d", result.code(), uid, iif);
+        }
+    }
+    return netdutils::status::ok;
+}
+
+Status TrafficController::removeUidInterfaceRules(const std::vector<int32_t>& uidsToDelete) {
+    std::lock_guard guard(mMutex);
+
+    for (auto uid : uidsToDelete) {
+        netdutils::Status result = removeRule(uid, IIF_MATCH);
+        if (!isOk(result)) {
+            ALOGW("removeRule failed(%d): uid=%d", result.code(), uid);
+        }
+    }
+    return netdutils::status::ok;
+}
+
+int TrafficController::replaceUidOwnerMap(const std::string& name, bool isAllowlist __unused,
+                                          const std::vector<int32_t>& uids) {
+    // FirewallRule rule = isAllowlist ? ALLOW : DENY;
+    // FirewallType type = isAllowlist ? ALLOWLIST : DENYLIST;
+    Status res;
+    if (!name.compare(LOCAL_DOZABLE)) {
+        res = replaceRulesInMap(DOZABLE_MATCH, uids);
+    } else if (!name.compare(LOCAL_STANDBY)) {
+        res = replaceRulesInMap(STANDBY_MATCH, uids);
+    } else if (!name.compare(LOCAL_POWERSAVE)) {
+        res = replaceRulesInMap(POWERSAVE_MATCH, uids);
+    } else if (!name.compare(LOCAL_RESTRICTED)) {
+        res = replaceRulesInMap(RESTRICTED_MATCH, uids);
+    } else {
+        ALOGE("unknown chain name: %s", name.c_str());
+        return -EINVAL;
+    }
+    if (!isOk(res)) {
+        ALOGE("Failed to clean up chain: %s: %s", name.c_str(), res.msg().c_str());
+        return -res.code();
+    }
+    return 0;
+}
+
+int TrafficController::toggleUidOwnerMap(ChildChain chain, bool enable) {
+    std::lock_guard guard(mMutex);
+    uint32_t key = UID_RULES_CONFIGURATION_KEY;
+    auto oldConfiguration = mConfigurationMap.readValue(key);
+    if (!oldConfiguration.ok()) {
+        ALOGE("Cannot read the old configuration from map: %s",
+              oldConfiguration.error().message().c_str());
+        return -oldConfiguration.error().code();
+    }
+    Status res;
+    BpfConfig newConfiguration;
+    uint8_t match;
+    switch (chain) {
+        case DOZABLE:
+            match = DOZABLE_MATCH;
+            break;
+        case STANDBY:
+            match = STANDBY_MATCH;
+            break;
+        case POWERSAVE:
+            match = POWERSAVE_MATCH;
+            break;
+        case RESTRICTED:
+            match = RESTRICTED_MATCH;
+            break;
+        default:
+            return -EINVAL;
+    }
+    newConfiguration =
+            enable ? (oldConfiguration.value() | match) : (oldConfiguration.value() & (~match));
+    res = mConfigurationMap.writeValue(key, newConfiguration, BPF_EXIST);
+    if (!isOk(res)) {
+        ALOGE("Failed to toggleUidOwnerMap(%d): %s", chain, res.msg().c_str());
+    }
+    return -res.code();
+}
+
+Status TrafficController::swapActiveStatsMap() {
+    std::lock_guard guard(mMutex);
+
+    uint32_t key = CURRENT_STATS_MAP_CONFIGURATION_KEY;
+    auto oldConfiguration = mConfigurationMap.readValue(key);
+    if (!oldConfiguration.ok()) {
+        ALOGE("Cannot read the old configuration from map: %s",
+              oldConfiguration.error().message().c_str());
+        return Status(oldConfiguration.error().code(), oldConfiguration.error().message());
+    }
+
+    // Write to the configuration map to inform the kernel eBPF program to switch
+    // from using one map to the other. Use flag BPF_EXIST here since the map should
+    // be already populated in initMaps.
+    uint8_t newConfigure = (oldConfiguration.value() == SELECT_MAP_A) ? SELECT_MAP_B : SELECT_MAP_A;
+    auto res = mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY, newConfigure,
+                                            BPF_EXIST);
+    if (!res.ok()) {
+        ALOGE("Failed to toggle the stats map: %s", strerror(res.error().code()));
+        return res;
+    }
+    // After changing the config, we need to make sure all the current running
+    // eBPF programs are finished and all the CPUs are aware of this config change
+    // before we modify the old map. So we do a special hack here to wait for
+    // the kernel to do a synchronize_rcu(). Once the kernel called
+    // synchronize_rcu(), the config we just updated will be available to all cores
+    // and the next eBPF programs triggered inside the kernel will use the new
+    // map configuration. So once this function returns we can safely modify the
+    // old stats map without concerning about race between the kernel and
+    // userspace.
+    int ret = synchronizeKernelRCU();
+    if (ret) {
+        ALOGE("map swap synchronize_rcu() ended with failure: %s", strerror(-ret));
+        return statusFromErrno(-ret, "map swap synchronize_rcu() failed");
+    }
+    return netdutils::status::ok;
+}
+
+void TrafficController::setPermissionForUids(int permission, const std::vector<uid_t>& uids) {
+    std::lock_guard guard(mMutex);
+    if (permission == INetd::PERMISSION_UNINSTALLED) {
+        for (uid_t uid : uids) {
+            // Clean up all permission information for the related uid if all the
+            // packages related to it are uninstalled.
+            mPrivilegedUser.erase(uid);
+            Status ret = mUidPermissionMap.deleteValue(uid);
+            if (!isOk(ret) && ret.code() != ENOENT) {
+                ALOGE("Failed to clean up the permission for %u: %s", uid, strerror(ret.code()));
+            }
+        }
+        return;
+    }
+
+    bool privileged = (permission & INetd::PERMISSION_UPDATE_DEVICE_STATS);
+
+    for (uid_t uid : uids) {
+        if (privileged) {
+            mPrivilegedUser.insert(uid);
+        } else {
+            mPrivilegedUser.erase(uid);
+        }
+
+        // The map stores all the permissions that the UID has, except if the only permission
+        // the UID has is the INTERNET permission, then the UID should not appear in the map.
+        if (permission != INetd::PERMISSION_INTERNET) {
+            Status ret = mUidPermissionMap.writeValue(uid, permission, BPF_ANY);
+            if (!isOk(ret)) {
+                ALOGE("Failed to set permission: %s of uid(%u) to permission map: %s",
+                      UidPermissionTypeToString(permission).c_str(), uid, strerror(ret.code()));
+            }
+        } else {
+            Status ret = mUidPermissionMap.deleteValue(uid);
+            if (!isOk(ret) && ret.code() != ENOENT) {
+                ALOGE("Failed to remove uid %u from permission map: %s", uid, strerror(ret.code()));
+            }
+        }
+    }
+}
+
+std::string getProgramStatus(const char *path) {
+    int ret = access(path, R_OK);
+    if (ret == 0) {
+        return StringPrintf("OK");
+    }
+    if (ret != 0 && errno == ENOENT) {
+        return StringPrintf("program is missing at: %s", path);
+    }
+    return StringPrintf("check Program %s error: %s", path, strerror(errno));
+}
+
+std::string getMapStatus(const base::unique_fd& map_fd, const char* path) {
+    if (map_fd.get() < 0) {
+        return StringPrintf("map fd lost");
+    }
+    if (access(path, F_OK) != 0) {
+        return StringPrintf("map not pinned to location: %s", path);
+    }
+    return StringPrintf("OK");
+}
+
+// NOLINTNEXTLINE(google-runtime-references): grandfathered pass by non-const reference
+void dumpBpfMap(const std::string& mapName, DumpWriter& dw, const std::string& header) {
+    dw.blankline();
+    dw.println("%s:", mapName.c_str());
+    if (!header.empty()) {
+        dw.println(header);
+    }
+}
+
+const String16 TrafficController::DUMP_KEYWORD = String16("trafficcontroller");
+
+void TrafficController::dump(DumpWriter& dw, bool verbose) {
+    std::lock_guard guard(mMutex);
+    ScopedIndent indentTop(dw);
+    dw.println("TrafficController");
+
+    ScopedIndent indentPreBpfModule(dw);
+
+    dw.blankline();
+    dw.println("mCookieTagMap status: %s",
+               getMapStatus(mCookieTagMap.getMap(), COOKIE_TAG_MAP_PATH).c_str());
+    dw.println("mUidCounterSetMap status: %s",
+               getMapStatus(mUidCounterSetMap.getMap(), UID_COUNTERSET_MAP_PATH).c_str());
+    dw.println("mAppUidStatsMap status: %s",
+               getMapStatus(mAppUidStatsMap.getMap(), APP_UID_STATS_MAP_PATH).c_str());
+    dw.println("mStatsMapA status: %s",
+               getMapStatus(mStatsMapA.getMap(), STATS_MAP_A_PATH).c_str());
+    dw.println("mStatsMapB status: %s",
+               getMapStatus(mStatsMapB.getMap(), STATS_MAP_B_PATH).c_str());
+    dw.println("mIfaceIndexNameMap status: %s",
+               getMapStatus(mIfaceIndexNameMap.getMap(), IFACE_INDEX_NAME_MAP_PATH).c_str());
+    dw.println("mIfaceStatsMap status: %s",
+               getMapStatus(mIfaceStatsMap.getMap(), IFACE_STATS_MAP_PATH).c_str());
+    dw.println("mConfigurationMap status: %s",
+               getMapStatus(mConfigurationMap.getMap(), CONFIGURATION_MAP_PATH).c_str());
+    dw.println("mUidOwnerMap status: %s",
+               getMapStatus(mUidOwnerMap.getMap(), UID_OWNER_MAP_PATH).c_str());
+
+    dw.blankline();
+    dw.println("Cgroup ingress program status: %s",
+               getProgramStatus(BPF_INGRESS_PROG_PATH).c_str());
+    dw.println("Cgroup egress program status: %s", getProgramStatus(BPF_EGRESS_PROG_PATH).c_str());
+    dw.println("xt_bpf ingress program status: %s",
+               getProgramStatus(XT_BPF_INGRESS_PROG_PATH).c_str());
+    dw.println("xt_bpf egress program status: %s",
+               getProgramStatus(XT_BPF_EGRESS_PROG_PATH).c_str());
+    dw.println("xt_bpf bandwidth allowlist program status: %s",
+               getProgramStatus(XT_BPF_ALLOWLIST_PROG_PATH).c_str());
+    dw.println("xt_bpf bandwidth denylist program status: %s",
+               getProgramStatus(XT_BPF_DENYLIST_PROG_PATH).c_str());
+
+    if (!verbose) {
+        return;
+    }
+
+    dw.blankline();
+    dw.println("BPF map content:");
+
+    ScopedIndent indentForMapContent(dw);
+
+    // Print CookieTagMap content.
+    dumpBpfMap("mCookieTagMap", dw, "");
+    const auto printCookieTagInfo = [&dw](const uint64_t& key, const UidTagValue& value,
+                                          const BpfMap<uint64_t, UidTagValue>&) {
+        dw.println("cookie=%" PRIu64 " tag=0x%x uid=%u", key, value.tag, value.uid);
+        return base::Result<void>();
+    };
+    base::Result<void> res = mCookieTagMap.iterateWithValue(printCookieTagInfo);
+    if (!res.ok()) {
+        dw.println("mCookieTagMap print end with error: %s", res.error().message().c_str());
+    }
+
+    // Print UidCounterSetMap content.
+    dumpBpfMap("mUidCounterSetMap", dw, "");
+    const auto printUidInfo = [&dw](const uint32_t& key, const uint8_t& value,
+                                    const BpfMap<uint32_t, uint8_t>&) {
+        dw.println("%u %u", key, value);
+        return base::Result<void>();
+    };
+    res = mUidCounterSetMap.iterateWithValue(printUidInfo);
+    if (!res.ok()) {
+        dw.println("mUidCounterSetMap print end with error: %s", res.error().message().c_str());
+    }
+
+    // Print AppUidStatsMap content.
+    std::string appUidStatsHeader = StringPrintf("uid rxBytes rxPackets txBytes txPackets");
+    dumpBpfMap("mAppUidStatsMap:", dw, appUidStatsHeader);
+    auto printAppUidStatsInfo = [&dw](const uint32_t& key, const StatsValue& value,
+                                      const BpfMap<uint32_t, StatsValue>&) {
+        dw.println("%u %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64, key, value.rxBytes,
+                   value.rxPackets, value.txBytes, value.txPackets);
+        return base::Result<void>();
+    };
+    res = mAppUidStatsMap.iterateWithValue(printAppUidStatsInfo);
+    if (!res.ok()) {
+        dw.println("mAppUidStatsMap print end with error: %s", res.error().message().c_str());
+    }
+
+    // Print uidStatsMap content.
+    std::string statsHeader = StringPrintf("ifaceIndex ifaceName tag_hex uid_int cnt_set rxBytes"
+                                           " rxPackets txBytes txPackets");
+    dumpBpfMap("mStatsMapA", dw, statsHeader);
+    const auto printStatsInfo = [&dw, this](const StatsKey& key, const StatsValue& value,
+                                            const BpfMap<StatsKey, StatsValue>&) {
+        uint32_t ifIndex = key.ifaceIndex;
+        auto ifname = mIfaceIndexNameMap.readValue(ifIndex);
+        if (!ifname.ok()) {
+            ifname = IfaceValue{"unknown"};
+        }
+        dw.println("%u %s 0x%x %u %u %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64, ifIndex,
+                   ifname.value().name, key.tag, key.uid, key.counterSet, value.rxBytes,
+                   value.rxPackets, value.txBytes, value.txPackets);
+        return base::Result<void>();
+    };
+    res = mStatsMapA.iterateWithValue(printStatsInfo);
+    if (!res.ok()) {
+        dw.println("mStatsMapA print end with error: %s", res.error().message().c_str());
+    }
+
+    // Print TagStatsMap content.
+    dumpBpfMap("mStatsMapB", dw, statsHeader);
+    res = mStatsMapB.iterateWithValue(printStatsInfo);
+    if (!res.ok()) {
+        dw.println("mStatsMapB print end with error: %s", res.error().message().c_str());
+    }
+
+    // Print ifaceIndexToNameMap content.
+    dumpBpfMap("mIfaceIndexNameMap", dw, "");
+    const auto printIfaceNameInfo = [&dw](const uint32_t& key, const IfaceValue& value,
+                                          const BpfMap<uint32_t, IfaceValue>&) {
+        const char* ifname = value.name;
+        dw.println("ifaceIndex=%u ifaceName=%s", key, ifname);
+        return base::Result<void>();
+    };
+    res = mIfaceIndexNameMap.iterateWithValue(printIfaceNameInfo);
+    if (!res.ok()) {
+        dw.println("mIfaceIndexNameMap print end with error: %s", res.error().message().c_str());
+    }
+
+    // Print ifaceStatsMap content
+    std::string ifaceStatsHeader = StringPrintf("ifaceIndex ifaceName rxBytes rxPackets txBytes"
+                                                " txPackets");
+    dumpBpfMap("mIfaceStatsMap:", dw, ifaceStatsHeader);
+    const auto printIfaceStatsInfo = [&dw, this](const uint32_t& key, const StatsValue& value,
+                                                 const BpfMap<uint32_t, StatsValue>&) {
+        auto ifname = mIfaceIndexNameMap.readValue(key);
+        if (!ifname.ok()) {
+            ifname = IfaceValue{"unknown"};
+        }
+        dw.println("%u %s %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64, key, ifname.value().name,
+                   value.rxBytes, value.rxPackets, value.txBytes, value.txPackets);
+        return base::Result<void>();
+    };
+    res = mIfaceStatsMap.iterateWithValue(printIfaceStatsInfo);
+    if (!res.ok()) {
+        dw.println("mIfaceStatsMap print end with error: %s", res.error().message().c_str());
+    }
+
+    dw.blankline();
+
+    uint32_t key = UID_RULES_CONFIGURATION_KEY;
+    auto configuration = mConfigurationMap.readValue(key);
+    if (configuration.ok()) {
+        dw.println("current ownerMatch configuration: %d%s", configuration.value(),
+                   uidMatchTypeToString(configuration.value()).c_str());
+    } else {
+        dw.println("mConfigurationMap read ownerMatch configure failed with error: %s",
+                   configuration.error().message().c_str());
+    }
+
+    key = CURRENT_STATS_MAP_CONFIGURATION_KEY;
+    configuration = mConfigurationMap.readValue(key);
+    if (configuration.ok()) {
+        const char* statsMapDescription = "???";
+        switch (configuration.value()) {
+            case SELECT_MAP_A:
+                statsMapDescription = "SELECT_MAP_A";
+                break;
+            case SELECT_MAP_B:
+                statsMapDescription = "SELECT_MAP_B";
+                break;
+                // No default clause, so if we ever add a third map, this code will fail to build.
+        }
+        dw.println("current statsMap configuration: %d %s", configuration.value(),
+                   statsMapDescription);
+    } else {
+        dw.println("mConfigurationMap read stats map configure failed with error: %s",
+                   configuration.error().message().c_str());
+    }
+    dumpBpfMap("mUidOwnerMap", dw, "");
+    const auto printUidMatchInfo = [&dw, this](const uint32_t& key, const UidOwnerValue& value,
+                                               const BpfMap<uint32_t, UidOwnerValue>&) {
+        if (value.rule & IIF_MATCH) {
+            auto ifname = mIfaceIndexNameMap.readValue(value.iif);
+            if (ifname.ok()) {
+                dw.println("%u %s %s", key, uidMatchTypeToString(value.rule).c_str(),
+                           ifname.value().name);
+            } else {
+                dw.println("%u %s %u", key, uidMatchTypeToString(value.rule).c_str(), value.iif);
+            }
+        } else {
+            dw.println("%u %s", key, uidMatchTypeToString(value.rule).c_str());
+        }
+        return base::Result<void>();
+    };
+    res = mUidOwnerMap.iterateWithValue(printUidMatchInfo);
+    if (!res.ok()) {
+        dw.println("mUidOwnerMap print end with error: %s", res.error().message().c_str());
+    }
+    dumpBpfMap("mUidPermissionMap", dw, "");
+    const auto printUidPermissionInfo = [&dw](const uint32_t& key, const int& value,
+                                              const BpfMap<uint32_t, uint8_t>&) {
+        dw.println("%u %s", key, UidPermissionTypeToString(value).c_str());
+        return base::Result<void>();
+    };
+    res = mUidPermissionMap.iterateWithValue(printUidPermissionInfo);
+    if (!res.ok()) {
+        dw.println("mUidPermissionMap print end with error: %s", res.error().message().c_str());
+    }
+
+    dumpBpfMap("mPrivilegedUser", dw, "");
+    for (uid_t uid : mPrivilegedUser) {
+        dw.println("%u ALLOW_UPDATE_DEVICE_STATS", (uint32_t)uid);
+    }
+}
+
+}  // namespace net
+}  // namespace android
diff --git a/service/native/TrafficControllerTest.cpp b/service/native/TrafficControllerTest.cpp
new file mode 100644
index 0000000..f401636
--- /dev/null
+++ b/service/native/TrafficControllerTest.cpp
@@ -0,0 +1,750 @@
+/*
+ * Copyright 2022 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.
+ *
+ * TrafficControllerTest.cpp - unit tests for TrafficController.cpp
+ */
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include <fcntl.h>
+#include <inttypes.h>
+#include <linux/inet_diag.h>
+#include <linux/sock_diag.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <gtest/gtest.h>
+
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+#include <binder/Status.h>
+
+#include <netdutils/MockSyscalls.h>
+
+#include "TrafficController.h"
+#include "bpf/BpfUtils.h"
+
+using namespace android::bpf;  // NOLINT(google-build-using-namespace): grandfathered
+
+namespace android {
+namespace net {
+
+using android::netdutils::Status;
+using base::Result;
+using netdutils::isOk;
+
+constexpr int TEST_MAP_SIZE = 10;
+constexpr uid_t TEST_UID = 10086;
+constexpr uid_t TEST_UID2 = 54321;
+constexpr uid_t TEST_UID3 = 98765;
+constexpr uint32_t TEST_TAG = 42;
+constexpr uint32_t TEST_COUNTERSET = 1;
+constexpr uint32_t DEFAULT_COUNTERSET = 0;
+
+#define ASSERT_VALID(x) ASSERT_TRUE((x).isValid())
+
+class TrafficControllerTest : public ::testing::Test {
+  protected:
+    TrafficControllerTest() {}
+    TrafficController mTc;
+    BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap;
+    BpfMap<uint32_t, uint8_t> mFakeUidCounterSetMap;
+    BpfMap<uint32_t, StatsValue> mFakeAppUidStatsMap;
+    BpfMap<StatsKey, StatsValue> mFakeStatsMapA;
+    BpfMap<uint32_t, uint8_t> mFakeConfigurationMap;
+    BpfMap<uint32_t, UidOwnerValue> mFakeUidOwnerMap;
+    BpfMap<uint32_t, uint8_t> mFakeUidPermissionMap;
+
+    void SetUp() {
+        std::lock_guard guard(mTc.mMutex);
+        ASSERT_EQ(0, setrlimitForTest());
+
+        mFakeCookieTagMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint64_t), sizeof(UidTagValue),
+                                          TEST_MAP_SIZE, 0));
+        ASSERT_VALID(mFakeCookieTagMap);
+
+        mFakeUidCounterSetMap.reset(
+            createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), TEST_MAP_SIZE, 0));
+        ASSERT_VALID(mFakeUidCounterSetMap);
+
+        mFakeAppUidStatsMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(StatsValue),
+                                            TEST_MAP_SIZE, 0));
+        ASSERT_VALID(mFakeAppUidStatsMap);
+
+        mFakeStatsMapA.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(StatsKey), sizeof(StatsValue),
+                                       TEST_MAP_SIZE, 0));
+        ASSERT_VALID(mFakeStatsMapA);
+
+        mFakeConfigurationMap.reset(
+                createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), 1, 0));
+        ASSERT_VALID(mFakeConfigurationMap);
+
+        mFakeUidOwnerMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(UidOwnerValue),
+                                         TEST_MAP_SIZE, 0));
+        ASSERT_VALID(mFakeUidOwnerMap);
+        mFakeUidPermissionMap.reset(
+                createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), TEST_MAP_SIZE, 0));
+        ASSERT_VALID(mFakeUidPermissionMap);
+
+        mTc.mCookieTagMap.reset(dupFd(mFakeCookieTagMap.getMap()));
+        ASSERT_VALID(mTc.mCookieTagMap);
+        mTc.mUidCounterSetMap.reset(dupFd(mFakeUidCounterSetMap.getMap()));
+        ASSERT_VALID(mTc.mUidCounterSetMap);
+        mTc.mAppUidStatsMap.reset(dupFd(mFakeAppUidStatsMap.getMap()));
+        ASSERT_VALID(mTc.mAppUidStatsMap);
+        mTc.mStatsMapA.reset(dupFd(mFakeStatsMapA.getMap()));
+        ASSERT_VALID(mTc.mStatsMapA);
+        mTc.mConfigurationMap.reset(dupFd(mFakeConfigurationMap.getMap()));
+        ASSERT_VALID(mTc.mConfigurationMap);
+
+        // Always write to stats map A by default.
+        ASSERT_RESULT_OK(mTc.mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY,
+                                                          SELECT_MAP_A, BPF_ANY));
+        mTc.mUidOwnerMap.reset(dupFd(mFakeUidOwnerMap.getMap()));
+        ASSERT_VALID(mTc.mUidOwnerMap);
+        mTc.mUidPermissionMap.reset(dupFd(mFakeUidPermissionMap.getMap()));
+        ASSERT_VALID(mTc.mUidPermissionMap);
+        mTc.mPrivilegedUser.clear();
+    }
+
+    int dupFd(const android::base::unique_fd& mapFd) {
+        return fcntl(mapFd.get(), F_DUPFD_CLOEXEC, 0);
+    }
+
+    void populateFakeStats(uint64_t cookie, uint32_t uid, uint32_t tag, StatsKey* key) {
+        UidTagValue cookieMapkey = {.uid = (uint32_t)uid, .tag = tag};
+        EXPECT_RESULT_OK(mFakeCookieTagMap.writeValue(cookie, cookieMapkey, BPF_ANY));
+        *key = {.uid = uid, .tag = tag, .counterSet = TEST_COUNTERSET, .ifaceIndex = 1};
+        StatsValue statsMapValue = {.rxPackets = 1, .rxBytes = 100};
+        uint8_t counterSet = TEST_COUNTERSET;
+        EXPECT_RESULT_OK(mFakeUidCounterSetMap.writeValue(uid, counterSet, BPF_ANY));
+        EXPECT_RESULT_OK(mFakeStatsMapA.writeValue(*key, statsMapValue, BPF_ANY));
+        key->tag = 0;
+        EXPECT_RESULT_OK(mFakeStatsMapA.writeValue(*key, statsMapValue, BPF_ANY));
+        EXPECT_RESULT_OK(mFakeAppUidStatsMap.writeValue(uid, statsMapValue, BPF_ANY));
+        // put tag information back to statsKey
+        key->tag = tag;
+    }
+
+    void checkUidOwnerRuleForChain(ChildChain chain, UidOwnerMatchType match) {
+        uint32_t uid = TEST_UID;
+        EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, DENY, DENYLIST));
+        Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid);
+        EXPECT_RESULT_OK(value);
+        EXPECT_TRUE(value.value().rule & match);
+
+        uid = TEST_UID2;
+        EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, ALLOW, ALLOWLIST));
+        value = mFakeUidOwnerMap.readValue(uid);
+        EXPECT_RESULT_OK(value);
+        EXPECT_TRUE(value.value().rule & match);
+
+        EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, DENY, ALLOWLIST));
+        value = mFakeUidOwnerMap.readValue(uid);
+        EXPECT_FALSE(value.ok());
+        EXPECT_EQ(ENOENT, value.error().code());
+
+        uid = TEST_UID;
+        EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, ALLOW, DENYLIST));
+        value = mFakeUidOwnerMap.readValue(uid);
+        EXPECT_FALSE(value.ok());
+        EXPECT_EQ(ENOENT, value.error().code());
+
+        uid = TEST_UID3;
+        EXPECT_EQ(-ENOENT, mTc.changeUidOwnerRule(chain, uid, ALLOW, DENYLIST));
+        value = mFakeUidOwnerMap.readValue(uid);
+        EXPECT_FALSE(value.ok());
+        EXPECT_EQ(ENOENT, value.error().code());
+    }
+
+    void checkEachUidValue(const std::vector<int32_t>& uids, UidOwnerMatchType match) {
+        for (uint32_t uid : uids) {
+            Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid);
+            EXPECT_RESULT_OK(value);
+            EXPECT_TRUE(value.value().rule & match);
+        }
+        std::set<uint32_t> uidSet(uids.begin(), uids.end());
+        const auto checkNoOtherUid = [&uidSet](const int32_t& key,
+                                               const BpfMap<uint32_t, UidOwnerValue>&) {
+            EXPECT_NE(uidSet.end(), uidSet.find(key));
+            return Result<void>();
+        };
+        EXPECT_RESULT_OK(mFakeUidOwnerMap.iterate(checkNoOtherUid));
+    }
+
+    void checkUidMapReplace(const std::string& name, const std::vector<int32_t>& uids,
+                            UidOwnerMatchType match) {
+        bool isAllowlist = true;
+        EXPECT_EQ(0, mTc.replaceUidOwnerMap(name, isAllowlist, uids));
+        checkEachUidValue(uids, match);
+
+        isAllowlist = false;
+        EXPECT_EQ(0, mTc.replaceUidOwnerMap(name, isAllowlist, uids));
+        checkEachUidValue(uids, match);
+    }
+
+    void expectUidOwnerMapValues(const std::vector<uint32_t>& appUids, uint8_t expectedRule,
+                                 uint32_t expectedIif) {
+        for (uint32_t uid : appUids) {
+            Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid);
+            EXPECT_RESULT_OK(value);
+            EXPECT_EQ(expectedRule, value.value().rule)
+                    << "Expected rule for UID " << uid << " to be " << expectedRule << ", but was "
+                    << value.value().rule;
+            EXPECT_EQ(expectedIif, value.value().iif)
+                    << "Expected iif for UID " << uid << " to be " << expectedIif << ", but was "
+                    << value.value().iif;
+        }
+    }
+
+    template <class Key, class Value>
+    void expectMapEmpty(BpfMap<Key, Value>& map) {
+        auto isEmpty = map.isEmpty();
+        EXPECT_RESULT_OK(isEmpty);
+        EXPECT_TRUE(isEmpty.value());
+    }
+
+    void expectUidPermissionMapValues(const std::vector<uid_t>& appUids, uint8_t expectedValue) {
+        for (uid_t uid : appUids) {
+            Result<uint8_t> value = mFakeUidPermissionMap.readValue(uid);
+            EXPECT_RESULT_OK(value);
+            EXPECT_EQ(expectedValue, value.value())
+                    << "Expected value for UID " << uid << " to be " << expectedValue
+                    << ", but was " << value.value();
+        }
+    }
+
+    void expectPrivilegedUserSet(const std::vector<uid_t>& appUids) {
+        std::lock_guard guard(mTc.mMutex);
+        EXPECT_EQ(appUids.size(), mTc.mPrivilegedUser.size());
+        for (uid_t uid : appUids) {
+            EXPECT_NE(mTc.mPrivilegedUser.end(), mTc.mPrivilegedUser.find(uid));
+        }
+    }
+
+    void expectPrivilegedUserSetEmpty() {
+        std::lock_guard guard(mTc.mMutex);
+        EXPECT_TRUE(mTc.mPrivilegedUser.empty());
+    }
+
+    void addPrivilegedUid(uid_t uid) {
+        std::vector privilegedUid = {uid};
+        mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, privilegedUid);
+    }
+
+    void removePrivilegedUid(uid_t uid) {
+        std::vector privilegedUid = {uid};
+        mTc.setPermissionForUids(INetd::PERMISSION_NONE, privilegedUid);
+    }
+
+    void expectFakeStatsUnchanged(uint64_t cookie, uint32_t tag, uint32_t uid,
+                                  StatsKey tagStatsMapKey) {
+        Result<UidTagValue> cookieMapResult = mFakeCookieTagMap.readValue(cookie);
+        EXPECT_RESULT_OK(cookieMapResult);
+        EXPECT_EQ(uid, cookieMapResult.value().uid);
+        EXPECT_EQ(tag, cookieMapResult.value().tag);
+        Result<uint8_t> counterSetResult = mFakeUidCounterSetMap.readValue(uid);
+        EXPECT_RESULT_OK(counterSetResult);
+        EXPECT_EQ(TEST_COUNTERSET, counterSetResult.value());
+        Result<StatsValue> statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey);
+        EXPECT_RESULT_OK(statsMapResult);
+        EXPECT_EQ((uint64_t)1, statsMapResult.value().rxPackets);
+        EXPECT_EQ((uint64_t)100, statsMapResult.value().rxBytes);
+        tagStatsMapKey.tag = 0;
+        statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey);
+        EXPECT_RESULT_OK(statsMapResult);
+        EXPECT_EQ((uint64_t)1, statsMapResult.value().rxPackets);
+        EXPECT_EQ((uint64_t)100, statsMapResult.value().rxBytes);
+        auto appStatsResult = mFakeAppUidStatsMap.readValue(uid);
+        EXPECT_RESULT_OK(appStatsResult);
+        EXPECT_EQ((uint64_t)1, appStatsResult.value().rxPackets);
+        EXPECT_EQ((uint64_t)100, appStatsResult.value().rxBytes);
+    }
+
+    Status updateUidOwnerMaps(const std::vector<uint32_t>& appUids,
+                              UidOwnerMatchType matchType, TrafficController::IptOp op) {
+        Status ret(0);
+        for (auto uid : appUids) {
+        ret = mTc.updateUidOwnerMap(uid, matchType, op);
+           if(!isOk(ret)) break;
+        }
+        return ret;
+    }
+
+};
+
+TEST_F(TrafficControllerTest, TestSetCounterSet) {
+    uid_t callingUid = TEST_UID2;
+    addPrivilegedUid(callingUid);
+    ASSERT_EQ(0, mTc.setCounterSet(TEST_COUNTERSET, TEST_UID, callingUid));
+    uid_t uid = TEST_UID;
+    Result<uint8_t> counterSetResult = mFakeUidCounterSetMap.readValue(uid);
+    ASSERT_RESULT_OK(counterSetResult);
+    ASSERT_EQ(TEST_COUNTERSET, counterSetResult.value());
+    ASSERT_EQ(0, mTc.setCounterSet(DEFAULT_COUNTERSET, TEST_UID, callingUid));
+    ASSERT_FALSE(mFakeUidCounterSetMap.readValue(uid).ok());
+    expectMapEmpty(mFakeUidCounterSetMap);
+}
+
+TEST_F(TrafficControllerTest, TestSetCounterSetWithoutPermission) {
+    ASSERT_EQ(-EPERM, mTc.setCounterSet(TEST_COUNTERSET, TEST_UID, TEST_UID2));
+    uid_t uid = TEST_UID;
+    ASSERT_FALSE(mFakeUidCounterSetMap.readValue(uid).ok());
+    expectMapEmpty(mFakeUidCounterSetMap);
+}
+
+TEST_F(TrafficControllerTest, TestSetInvalidCounterSet) {
+    uid_t callingUid = TEST_UID2;
+    addPrivilegedUid(callingUid);
+    ASSERT_GT(0, mTc.setCounterSet(OVERFLOW_COUNTERSET, TEST_UID, callingUid));
+    uid_t uid = TEST_UID;
+    ASSERT_FALSE(mFakeUidCounterSetMap.readValue(uid).ok());
+    expectMapEmpty(mFakeUidCounterSetMap);
+}
+
+TEST_F(TrafficControllerTest, TestDeleteTagDataWithoutPermission) {
+    uint64_t cookie = 1;
+    uid_t uid = TEST_UID;
+    uint32_t tag = TEST_TAG;
+    StatsKey tagStatsMapKey;
+    populateFakeStats(cookie, uid, tag, &tagStatsMapKey);
+    ASSERT_EQ(-EPERM, mTc.deleteTagData(0, TEST_UID, TEST_UID2));
+
+    expectFakeStatsUnchanged(cookie, tag, uid, tagStatsMapKey);
+}
+
+TEST_F(TrafficControllerTest, TestDeleteTagData) {
+    uid_t callingUid = TEST_UID2;
+    addPrivilegedUid(callingUid);
+    uint64_t cookie = 1;
+    uid_t uid = TEST_UID;
+    uint32_t tag = TEST_TAG;
+    StatsKey tagStatsMapKey;
+    populateFakeStats(cookie, uid, tag, &tagStatsMapKey);
+    ASSERT_EQ(0, mTc.deleteTagData(TEST_TAG, TEST_UID, callingUid));
+    ASSERT_FALSE(mFakeCookieTagMap.readValue(cookie).ok());
+    Result<uint8_t> counterSetResult = mFakeUidCounterSetMap.readValue(uid);
+    ASSERT_RESULT_OK(counterSetResult);
+    ASSERT_EQ(TEST_COUNTERSET, counterSetResult.value());
+    ASSERT_FALSE(mFakeStatsMapA.readValue(tagStatsMapKey).ok());
+    tagStatsMapKey.tag = 0;
+    Result<StatsValue> statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey);
+    ASSERT_RESULT_OK(statsMapResult);
+    ASSERT_EQ((uint64_t)1, statsMapResult.value().rxPackets);
+    ASSERT_EQ((uint64_t)100, statsMapResult.value().rxBytes);
+    auto appStatsResult = mFakeAppUidStatsMap.readValue(TEST_UID);
+    ASSERT_RESULT_OK(appStatsResult);
+    ASSERT_EQ((uint64_t)1, appStatsResult.value().rxPackets);
+    ASSERT_EQ((uint64_t)100, appStatsResult.value().rxBytes);
+}
+
+TEST_F(TrafficControllerTest, TestDeleteAllUidData) {
+    uid_t callingUid = TEST_UID2;
+    addPrivilegedUid(callingUid);
+    uint64_t cookie = 1;
+    uid_t uid = TEST_UID;
+    uint32_t tag = TEST_TAG;
+    StatsKey tagStatsMapKey;
+    populateFakeStats(cookie, uid, tag, &tagStatsMapKey);
+    ASSERT_EQ(0, mTc.deleteTagData(0, TEST_UID, callingUid));
+    ASSERT_FALSE(mFakeCookieTagMap.readValue(cookie).ok());
+    ASSERT_FALSE(mFakeUidCounterSetMap.readValue(uid).ok());
+    ASSERT_FALSE(mFakeStatsMapA.readValue(tagStatsMapKey).ok());
+    tagStatsMapKey.tag = 0;
+    ASSERT_FALSE(mFakeStatsMapA.readValue(tagStatsMapKey).ok());
+    ASSERT_FALSE(mFakeAppUidStatsMap.readValue(TEST_UID).ok());
+}
+
+TEST_F(TrafficControllerTest, TestDeleteDataWithTwoTags) {
+    uid_t callingUid = TEST_UID2;
+    addPrivilegedUid(callingUid);
+    uint64_t cookie1 = 1;
+    uint64_t cookie2 = 2;
+    uid_t uid = TEST_UID;
+    uint32_t tag1 = TEST_TAG;
+    uint32_t tag2 = TEST_TAG + 1;
+    StatsKey tagStatsMapKey1;
+    StatsKey tagStatsMapKey2;
+    populateFakeStats(cookie1, uid, tag1, &tagStatsMapKey1);
+    populateFakeStats(cookie2, uid, tag2, &tagStatsMapKey2);
+    ASSERT_EQ(0, mTc.deleteTagData(TEST_TAG, TEST_UID, callingUid));
+    ASSERT_FALSE(mFakeCookieTagMap.readValue(cookie1).ok());
+    Result<UidTagValue> cookieMapResult = mFakeCookieTagMap.readValue(cookie2);
+    ASSERT_RESULT_OK(cookieMapResult);
+    ASSERT_EQ(TEST_UID, cookieMapResult.value().uid);
+    ASSERT_EQ(TEST_TAG + 1, cookieMapResult.value().tag);
+    Result<uint8_t> counterSetResult = mFakeUidCounterSetMap.readValue(uid);
+    ASSERT_RESULT_OK(counterSetResult);
+    ASSERT_EQ(TEST_COUNTERSET, counterSetResult.value());
+    ASSERT_FALSE(mFakeStatsMapA.readValue(tagStatsMapKey1).ok());
+    Result<StatsValue> statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey2);
+    ASSERT_RESULT_OK(statsMapResult);
+    ASSERT_EQ((uint64_t)1, statsMapResult.value().rxPackets);
+    ASSERT_EQ((uint64_t)100, statsMapResult.value().rxBytes);
+}
+
+TEST_F(TrafficControllerTest, TestDeleteDataWithTwoUids) {
+    uid_t callingUid = TEST_UID2;
+    addPrivilegedUid(callingUid);
+    uint64_t cookie1 = 1;
+    uint64_t cookie2 = 2;
+    uid_t uid1 = TEST_UID;
+    uid_t uid2 = TEST_UID + 1;
+    uint32_t tag = TEST_TAG;
+    StatsKey tagStatsMapKey1;
+    StatsKey tagStatsMapKey2;
+    populateFakeStats(cookie1, uid1, tag, &tagStatsMapKey1);
+    populateFakeStats(cookie2, uid2, tag, &tagStatsMapKey2);
+
+    // Delete the stats of one of the uid. Check if it is properly collected by
+    // removedStats.
+    ASSERT_EQ(0, mTc.deleteTagData(0, uid2, callingUid));
+    ASSERT_FALSE(mFakeCookieTagMap.readValue(cookie2).ok());
+    Result<uint8_t> counterSetResult = mFakeUidCounterSetMap.readValue(uid1);
+    ASSERT_RESULT_OK(counterSetResult);
+    ASSERT_EQ(TEST_COUNTERSET, counterSetResult.value());
+    ASSERT_FALSE(mFakeUidCounterSetMap.readValue(uid2).ok());
+    ASSERT_FALSE(mFakeStatsMapA.readValue(tagStatsMapKey2).ok());
+    tagStatsMapKey2.tag = 0;
+    ASSERT_FALSE(mFakeStatsMapA.readValue(tagStatsMapKey2).ok());
+    ASSERT_FALSE(mFakeAppUidStatsMap.readValue(uid2).ok());
+    tagStatsMapKey1.tag = 0;
+    Result<StatsValue> statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey1);
+    ASSERT_RESULT_OK(statsMapResult);
+    ASSERT_EQ((uint64_t)1, statsMapResult.value().rxPackets);
+    ASSERT_EQ((uint64_t)100, statsMapResult.value().rxBytes);
+    auto appStatsResult = mFakeAppUidStatsMap.readValue(uid1);
+    ASSERT_RESULT_OK(appStatsResult);
+    ASSERT_EQ((uint64_t)1, appStatsResult.value().rxPackets);
+    ASSERT_EQ((uint64_t)100, appStatsResult.value().rxBytes);
+
+    // Delete the stats of the other uid.
+    ASSERT_EQ(0, mTc.deleteTagData(0, uid1, callingUid));
+    ASSERT_FALSE(mFakeStatsMapA.readValue(tagStatsMapKey1).ok());
+    ASSERT_FALSE(mFakeAppUidStatsMap.readValue(uid1).ok());
+}
+
+TEST_F(TrafficControllerTest, TestUpdateOwnerMapEntry) {
+    uint32_t uid = TEST_UID;
+    ASSERT_TRUE(isOk(mTc.updateOwnerMapEntry(STANDBY_MATCH, uid, DENY, DENYLIST)));
+    Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid);
+    ASSERT_RESULT_OK(value);
+    ASSERT_TRUE(value.value().rule & STANDBY_MATCH);
+
+    ASSERT_TRUE(isOk(mTc.updateOwnerMapEntry(DOZABLE_MATCH, uid, ALLOW, ALLOWLIST)));
+    value = mFakeUidOwnerMap.readValue(uid);
+    ASSERT_RESULT_OK(value);
+    ASSERT_TRUE(value.value().rule & DOZABLE_MATCH);
+
+    ASSERT_TRUE(isOk(mTc.updateOwnerMapEntry(DOZABLE_MATCH, uid, DENY, ALLOWLIST)));
+    value = mFakeUidOwnerMap.readValue(uid);
+    ASSERT_RESULT_OK(value);
+    ASSERT_FALSE(value.value().rule & DOZABLE_MATCH);
+
+    ASSERT_TRUE(isOk(mTc.updateOwnerMapEntry(STANDBY_MATCH, uid, ALLOW, DENYLIST)));
+    ASSERT_FALSE(mFakeUidOwnerMap.readValue(uid).ok());
+
+    uid = TEST_UID2;
+    ASSERT_FALSE(isOk(mTc.updateOwnerMapEntry(STANDBY_MATCH, uid, ALLOW, DENYLIST)));
+    ASSERT_FALSE(mFakeUidOwnerMap.readValue(uid).ok());
+}
+
+TEST_F(TrafficControllerTest, TestChangeUidOwnerRule) {
+    checkUidOwnerRuleForChain(DOZABLE, DOZABLE_MATCH);
+    checkUidOwnerRuleForChain(STANDBY, STANDBY_MATCH);
+    checkUidOwnerRuleForChain(POWERSAVE, POWERSAVE_MATCH);
+    checkUidOwnerRuleForChain(RESTRICTED, RESTRICTED_MATCH);
+    ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(NONE, TEST_UID, ALLOW, ALLOWLIST));
+    ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(INVALID_CHAIN, TEST_UID, ALLOW, ALLOWLIST));
+}
+
+TEST_F(TrafficControllerTest, TestReplaceUidOwnerMap) {
+    std::vector<int32_t> uids = {TEST_UID, TEST_UID2, TEST_UID3};
+    checkUidMapReplace("fw_dozable", uids, DOZABLE_MATCH);
+    checkUidMapReplace("fw_standby", uids, STANDBY_MATCH);
+    checkUidMapReplace("fw_powersave", uids, POWERSAVE_MATCH);
+    checkUidMapReplace("fw_restricted", uids, RESTRICTED_MATCH);
+    ASSERT_EQ(-EINVAL, mTc.replaceUidOwnerMap("unknow", true, uids));
+}
+
+TEST_F(TrafficControllerTest, TestReplaceSameChain) {
+    std::vector<int32_t> uids = {TEST_UID, TEST_UID2, TEST_UID3};
+    checkUidMapReplace("fw_dozable", uids, DOZABLE_MATCH);
+    std::vector<int32_t> newUids = {TEST_UID2, TEST_UID3};
+    checkUidMapReplace("fw_dozable", newUids, DOZABLE_MATCH);
+}
+
+TEST_F(TrafficControllerTest, TestDenylistUidMatch) {
+    std::vector<uint32_t> appUids = {1000, 1001, 10012};
+    ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH,
+                                        TrafficController::IptOpInsert)));
+    expectUidOwnerMapValues(appUids, PENALTY_BOX_MATCH, 0);
+    ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH,
+                                        TrafficController::IptOpDelete)));
+    expectMapEmpty(mFakeUidOwnerMap);
+}
+
+TEST_F(TrafficControllerTest, TestAllowlistUidMatch) {
+    std::vector<uint32_t> appUids = {1000, 1001, 10012};
+    ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, TrafficController::IptOpInsert)));
+    expectUidOwnerMapValues(appUids, HAPPY_BOX_MATCH, 0);
+    ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, TrafficController::IptOpDelete)));
+    expectMapEmpty(mFakeUidOwnerMap);
+}
+
+TEST_F(TrafficControllerTest, TestReplaceMatchUid) {
+    std::vector<uint32_t> appUids = {1000, 1001, 10012};
+    // Add appUids to the denylist and expect that their values are all PENALTY_BOX_MATCH.
+    ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH,
+                                        TrafficController::IptOpInsert)));
+    expectUidOwnerMapValues(appUids, PENALTY_BOX_MATCH, 0);
+
+    // Add the same UIDs to the allowlist and expect that we get PENALTY_BOX_MATCH |
+    // HAPPY_BOX_MATCH.
+    ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, TrafficController::IptOpInsert)));
+    expectUidOwnerMapValues(appUids, HAPPY_BOX_MATCH | PENALTY_BOX_MATCH, 0);
+
+    // Remove the same UIDs from the allowlist and check the PENALTY_BOX_MATCH is still there.
+    ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, TrafficController::IptOpDelete)));
+    expectUidOwnerMapValues(appUids, PENALTY_BOX_MATCH, 0);
+
+    // Remove the same UIDs from the denylist and check the map is empty.
+    ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH,
+                                        TrafficController::IptOpDelete)));
+    ASSERT_FALSE(mFakeUidOwnerMap.getFirstKey().ok());
+}
+
+TEST_F(TrafficControllerTest, TestDeleteWrongMatchSilentlyFails) {
+    std::vector<uint32_t> appUids = {1000, 1001, 10012};
+    // If the uid does not exist in the map, trying to delete a rule about it will fail.
+    ASSERT_FALSE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH,
+                                         TrafficController::IptOpDelete)));
+    expectMapEmpty(mFakeUidOwnerMap);
+
+    // Add denylist rules for appUids.
+    ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH,
+                                        TrafficController::IptOpInsert)));
+    expectUidOwnerMapValues(appUids, HAPPY_BOX_MATCH, 0);
+
+    // Delete (non-existent) denylist rules for appUids, and check that this silently does
+    // nothing if the uid is in the map but does not have denylist match. This is required because
+    // NetworkManagementService will try to remove a uid from denylist after adding it to the
+    // allowlist and if the remove fails it will not update the uid status.
+    ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH,
+                                        TrafficController::IptOpDelete)));
+    expectUidOwnerMapValues(appUids, HAPPY_BOX_MATCH, 0);
+}
+
+TEST_F(TrafficControllerTest, TestAddUidInterfaceFilteringRules) {
+    int iif0 = 15;
+    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif0, {1000, 1001})));
+    expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif0);
+
+    // Add some non-overlapping new uids. They should coexist with existing rules
+    int iif1 = 16;
+    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif1, {2000, 2001})));
+    expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif0);
+    expectUidOwnerMapValues({2000, 2001}, IIF_MATCH, iif1);
+
+    // Overwrite some existing uids
+    int iif2 = 17;
+    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif2, {1000, 2000})));
+    expectUidOwnerMapValues({1001}, IIF_MATCH, iif0);
+    expectUidOwnerMapValues({2001}, IIF_MATCH, iif1);
+    expectUidOwnerMapValues({1000, 2000}, IIF_MATCH, iif2);
+}
+
+TEST_F(TrafficControllerTest, TestRemoveUidInterfaceFilteringRules) {
+    int iif0 = 15;
+    int iif1 = 16;
+    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif0, {1000, 1001})));
+    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif1, {2000, 2001})));
+    expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif0);
+    expectUidOwnerMapValues({2000, 2001}, IIF_MATCH, iif1);
+
+    // Rmove some uids
+    ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1001, 2001})));
+    expectUidOwnerMapValues({1000}, IIF_MATCH, iif0);
+    expectUidOwnerMapValues({2000}, IIF_MATCH, iif1);
+    checkEachUidValue({1000, 2000}, IIF_MATCH);  // Make sure there are only two uids remaining
+
+    // Remove non-existent uids shouldn't fail
+    ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({2000, 3000})));
+    expectUidOwnerMapValues({1000}, IIF_MATCH, iif0);
+    checkEachUidValue({1000}, IIF_MATCH);  // Make sure there are only one uid remaining
+
+    // Remove everything
+    ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
+    expectMapEmpty(mFakeUidOwnerMap);
+}
+
+TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesCoexistWithExistingMatches) {
+    // Set up existing PENALTY_BOX_MATCH rules
+    ASSERT_TRUE(isOk(updateUidOwnerMaps({1000, 1001, 10012}, PENALTY_BOX_MATCH,
+                                        TrafficController::IptOpInsert)));
+    expectUidOwnerMapValues({1000, 1001, 10012}, PENALTY_BOX_MATCH, 0);
+
+    // Add some partially-overlapping uid owner rules and check result
+    int iif1 = 32;
+    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif1, {10012, 10013, 10014})));
+    expectUidOwnerMapValues({1000, 1001}, PENALTY_BOX_MATCH, 0);
+    expectUidOwnerMapValues({10012}, PENALTY_BOX_MATCH | IIF_MATCH, iif1);
+    expectUidOwnerMapValues({10013, 10014}, IIF_MATCH, iif1);
+
+    // Removing some PENALTY_BOX_MATCH rules should not change uid interface rule
+    ASSERT_TRUE(isOk(updateUidOwnerMaps({1001, 10012}, PENALTY_BOX_MATCH,
+                                        TrafficController::IptOpDelete)));
+    expectUidOwnerMapValues({1000}, PENALTY_BOX_MATCH, 0);
+    expectUidOwnerMapValues({10012, 10013, 10014}, IIF_MATCH, iif1);
+
+    // Remove all uid interface rules
+    ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({10012, 10013, 10014})));
+    expectUidOwnerMapValues({1000}, PENALTY_BOX_MATCH, 0);
+    // Make sure these are the only uids left
+    checkEachUidValue({1000}, PENALTY_BOX_MATCH);
+}
+
+TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesCoexistWithNewMatches) {
+    int iif1 = 56;
+    // Set up existing uid interface rules
+    ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif1, {10001, 10002})));
+    expectUidOwnerMapValues({10001, 10002}, IIF_MATCH, iif1);
+
+    // Add some partially-overlapping doze rules
+    EXPECT_EQ(0, mTc.replaceUidOwnerMap("fw_dozable", true, {10002, 10003}));
+    expectUidOwnerMapValues({10001}, IIF_MATCH, iif1);
+    expectUidOwnerMapValues({10002}, DOZABLE_MATCH | IIF_MATCH, iif1);
+    expectUidOwnerMapValues({10003}, DOZABLE_MATCH, 0);
+
+    // Introduce a third rule type (powersave) on various existing UIDs
+    EXPECT_EQ(0, mTc.replaceUidOwnerMap("fw_powersave", true, {10000, 10001, 10002, 10003}));
+    expectUidOwnerMapValues({10000}, POWERSAVE_MATCH, 0);
+    expectUidOwnerMapValues({10001}, POWERSAVE_MATCH | IIF_MATCH, iif1);
+    expectUidOwnerMapValues({10002}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif1);
+    expectUidOwnerMapValues({10003}, POWERSAVE_MATCH | DOZABLE_MATCH, 0);
+
+    // Remove all doze rules
+    EXPECT_EQ(0, mTc.replaceUidOwnerMap("fw_dozable", true, {}));
+    expectUidOwnerMapValues({10000}, POWERSAVE_MATCH, 0);
+    expectUidOwnerMapValues({10001}, POWERSAVE_MATCH | IIF_MATCH, iif1);
+    expectUidOwnerMapValues({10002}, POWERSAVE_MATCH | IIF_MATCH, iif1);
+    expectUidOwnerMapValues({10003}, POWERSAVE_MATCH, 0);
+
+    // Remove all powersave rules, expect ownerMap to only have uid interface rules left
+    EXPECT_EQ(0, mTc.replaceUidOwnerMap("fw_powersave", true, {}));
+    expectUidOwnerMapValues({10001, 10002}, IIF_MATCH, iif1);
+    // Make sure these are the only uids left
+    checkEachUidValue({10001, 10002}, IIF_MATCH);
+}
+
+TEST_F(TrafficControllerTest, TestGrantInternetPermission) {
+    std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
+
+    mTc.setPermissionForUids(INetd::PERMISSION_INTERNET, appUids);
+    expectMapEmpty(mFakeUidPermissionMap);
+    expectPrivilegedUserSetEmpty();
+}
+
+TEST_F(TrafficControllerTest, TestRevokeInternetPermission) {
+    std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
+
+    mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids);
+    expectUidPermissionMapValues(appUids, INetd::PERMISSION_NONE);
+}
+
+TEST_F(TrafficControllerTest, TestPermissionUninstalled) {
+    std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
+
+    mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, appUids);
+    expectUidPermissionMapValues(appUids, INetd::PERMISSION_UPDATE_DEVICE_STATS);
+    expectPrivilegedUserSet(appUids);
+
+    std::vector<uid_t> uidToRemove = {TEST_UID};
+    mTc.setPermissionForUids(INetd::PERMISSION_UNINSTALLED, uidToRemove);
+
+    std::vector<uid_t> uidRemain = {TEST_UID3, TEST_UID2};
+    expectUidPermissionMapValues(uidRemain, INetd::PERMISSION_UPDATE_DEVICE_STATS);
+    expectPrivilegedUserSet(uidRemain);
+
+    mTc.setPermissionForUids(INetd::PERMISSION_UNINSTALLED, uidRemain);
+    expectMapEmpty(mFakeUidPermissionMap);
+    expectPrivilegedUserSetEmpty();
+}
+
+TEST_F(TrafficControllerTest, TestGrantUpdateStatsPermission) {
+    std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
+
+    mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, appUids);
+    expectUidPermissionMapValues(appUids, INetd::PERMISSION_UPDATE_DEVICE_STATS);
+    expectPrivilegedUserSet(appUids);
+
+    mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids);
+    expectPrivilegedUserSetEmpty();
+    expectUidPermissionMapValues(appUids, INetd::PERMISSION_NONE);
+}
+
+TEST_F(TrafficControllerTest, TestRevokeUpdateStatsPermission) {
+    std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
+
+    mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, appUids);
+    expectPrivilegedUserSet(appUids);
+
+    std::vector<uid_t> uidToRemove = {TEST_UID};
+    mTc.setPermissionForUids(INetd::PERMISSION_NONE, uidToRemove);
+
+    std::vector<uid_t> uidRemain = {TEST_UID3, TEST_UID2};
+    expectPrivilegedUserSet(uidRemain);
+
+    mTc.setPermissionForUids(INetd::PERMISSION_NONE, uidRemain);
+    expectPrivilegedUserSetEmpty();
+}
+
+TEST_F(TrafficControllerTest, TestGrantWrongPermission) {
+    std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
+
+    mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids);
+    expectPrivilegedUserSetEmpty();
+    expectUidPermissionMapValues(appUids, INetd::PERMISSION_NONE);
+}
+
+TEST_F(TrafficControllerTest, TestGrantDuplicatePermissionSlientlyFail) {
+    std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
+
+    mTc.setPermissionForUids(INetd::PERMISSION_INTERNET, appUids);
+    expectMapEmpty(mFakeUidPermissionMap);
+
+    std::vector<uid_t> uidToAdd = {TEST_UID};
+    mTc.setPermissionForUids(INetd::PERMISSION_INTERNET, uidToAdd);
+
+    expectPrivilegedUserSetEmpty();
+
+    mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids);
+    expectUidPermissionMapValues(appUids, INetd::PERMISSION_NONE);
+
+    mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, appUids);
+    expectPrivilegedUserSet(appUids);
+
+    mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, uidToAdd);
+    expectPrivilegedUserSet(appUids);
+
+    mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids);
+    expectPrivilegedUserSetEmpty();
+}
+
+}  // namespace net
+}  // namespace android
diff --git a/service/native/include/Common.h b/service/native/include/Common.h
new file mode 100644
index 0000000..7c0b797
--- /dev/null
+++ b/service/native/include/Common.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+#pragma once
+// TODO: deduplicate with the constants in NetdConstants.h.
+#include <aidl/android/net/INetd.h>
+
+using aidl::android::net::INetd;
+
+enum FirewallRule { ALLOW = INetd::FIREWALL_RULE_ALLOW, DENY = INetd::FIREWALL_RULE_DENY };
+
+// ALLOWLIST means the firewall denies all by default, uids must be explicitly ALLOWed
+// DENYLIST means the firewall allows all by default, uids must be explicitly DENYed
+
+enum FirewallType { ALLOWLIST = INetd::FIREWALL_ALLOWLIST, DENYLIST = INetd::FIREWALL_DENYLIST };
+
+enum ChildChain {
+    NONE = INetd::FIREWALL_CHAIN_NONE,
+    DOZABLE = INetd::FIREWALL_CHAIN_DOZABLE,
+    STANDBY = INetd::FIREWALL_CHAIN_STANDBY,
+    POWERSAVE = INetd::FIREWALL_CHAIN_POWERSAVE,
+    RESTRICTED = INetd::FIREWALL_CHAIN_RESTRICTED,
+    INVALID_CHAIN
+};
diff --git a/service/native/include/TrafficController.h b/service/native/include/TrafficController.h
new file mode 100644
index 0000000..c050871
--- /dev/null
+++ b/service/native/include/TrafficController.h
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+#pragma once
+
+#include <set>
+#include <Common.h>
+
+#include "android-base/thread_annotations.h"
+#include "bpf/BpfMap.h"
+#include "bpf_shared.h"
+#include "netdutils/DumpWriter.h"
+#include "netdutils/NetlinkListener.h"
+#include "netdutils/StatusOr.h"
+#include "utils/String16.h"
+
+namespace android {
+namespace net {
+
+using netdutils::StatusOr;
+
+class TrafficController {
+  public:
+    /*
+     * Initialize the whole controller
+     */
+    netdutils::Status start();
+
+    int setCounterSet(int counterSetNum, uid_t uid, uid_t callingUid) EXCLUDES(mMutex);
+
+    /*
+     * When deleting a tag data, the qtaguid module will grab the spinlock of each
+     * related rb_tree one by one and delete the tag information, counterSet
+     * information, iface stats information and uid stats information one by one.
+     * The new eBPF implementation is done similiarly by removing the entry on
+     * each map one by one. And deleting processes are also protected by the
+     * spinlock of the map. So no additional lock is required.
+     */
+    int deleteTagData(uint32_t tag, uid_t uid, uid_t callingUid) EXCLUDES(mMutex);
+
+    /*
+     * Swap the stats map config from current active stats map to the idle one.
+     */
+    netdutils::Status swapActiveStatsMap() EXCLUDES(mMutex);
+
+    /*
+     * Add the interface name and index pair into the eBPF map.
+     */
+    int addInterface(const char* name, uint32_t ifaceIndex);
+
+    int changeUidOwnerRule(ChildChain chain, const uid_t uid, FirewallRule rule, FirewallType type);
+
+    int removeUidOwnerRule(const uid_t uid);
+
+    int replaceUidOwnerMap(const std::string& name, bool isAllowlist,
+                           const std::vector<int32_t>& uids);
+
+    enum IptOp { IptOpInsert, IptOpDelete };
+
+    netdutils::Status updateOwnerMapEntry(UidOwnerMatchType match, uid_t uid, FirewallRule rule,
+                                          FirewallType type) EXCLUDES(mMutex);
+
+    void dump(netdutils::DumpWriter& dw, bool verbose) EXCLUDES(mMutex);
+
+    netdutils::Status replaceRulesInMap(UidOwnerMatchType match, const std::vector<int32_t>& uids)
+            EXCLUDES(mMutex);
+
+    netdutils::Status addUidInterfaceRules(const int ifIndex, const std::vector<int32_t>& uids)
+            EXCLUDES(mMutex);
+    netdutils::Status removeUidInterfaceRules(const std::vector<int32_t>& uids) EXCLUDES(mMutex);
+
+    netdutils::Status updateUidOwnerMap(const uint32_t uid,
+                                        UidOwnerMatchType matchType, IptOp op) EXCLUDES(mMutex);
+    static const String16 DUMP_KEYWORD;
+
+    int toggleUidOwnerMap(ChildChain chain, bool enable) EXCLUDES(mMutex);
+
+    static netdutils::StatusOr<std::unique_ptr<netdutils::NetlinkListenerInterface>>
+    makeSkDestroyListener();
+
+    void setPermissionForUids(int permission, const std::vector<uid_t>& uids) EXCLUDES(mMutex);
+
+    FirewallType getFirewallType(ChildChain);
+
+    static const char* LOCAL_DOZABLE;
+    static const char* LOCAL_STANDBY;
+    static const char* LOCAL_POWERSAVE;
+    static const char* LOCAL_RESTRICTED;
+
+  private:
+    /*
+     * mCookieTagMap: Store the corresponding tag and uid for a specific socket.
+     * DO NOT hold any locks when modifying this map, otherwise when the untag
+     * operation is waiting for a lock hold by other process and there are more
+     * sockets being closed than can fit in the socket buffer of the netlink socket
+     * that receives them, then the kernel will drop some of these sockets and we
+     * won't delete their tags.
+     * Map Key: uint64_t socket cookie
+     * Map Value: UidTagValue, contains a uint32 uid and a uint32 tag.
+     */
+    bpf::BpfMap<uint64_t, UidTagValue> mCookieTagMap GUARDED_BY(mMutex);
+
+    /*
+     * mUidCounterSetMap: Store the counterSet of a specific uid.
+     * Map Key: uint32 uid.
+     * Map Value: uint32 counterSet specifies if the traffic is a background
+     * or foreground traffic.
+     */
+    bpf::BpfMap<uint32_t, uint8_t> mUidCounterSetMap GUARDED_BY(mMutex);
+
+    /*
+     * mAppUidStatsMap: Store the total traffic stats for a uid regardless of
+     * tag, counterSet and iface. The stats is used by TrafficStats.getUidStats
+     * API to return persistent stats for a specific uid since device boot.
+     */
+    bpf::BpfMap<uint32_t, StatsValue> mAppUidStatsMap;
+
+    /*
+     * mStatsMapA/mStatsMapB: Store the traffic statistics for a specific
+     * combination of uid, tag, iface and counterSet. These two maps contain
+     * both tagged and untagged traffic.
+     * Map Key: StatsKey contains the uid, tag, counterSet and ifaceIndex
+     * information.
+     * Map Value: Stats, contains packet count and byte count of each
+     * transport protocol on egress and ingress direction.
+     */
+    bpf::BpfMap<StatsKey, StatsValue> mStatsMapA GUARDED_BY(mMutex);
+
+    bpf::BpfMap<StatsKey, StatsValue> mStatsMapB GUARDED_BY(mMutex);
+
+    /*
+     * mIfaceIndexNameMap: Store the index name pair of each interface show up
+     * on the device since boot. The interface index is used by the eBPF program
+     * to correctly match the iface name when receiving a packet.
+     */
+    bpf::BpfMap<uint32_t, IfaceValue> mIfaceIndexNameMap;
+
+    /*
+     * mIfaceStataMap: Store per iface traffic stats gathered from xt_bpf
+     * filter.
+     */
+    bpf::BpfMap<uint32_t, StatsValue> mIfaceStatsMap;
+
+    /*
+     * mConfigurationMap: Store the current network policy about uid filtering
+     * and the current stats map in use. There are two configuration entries in
+     * the map right now:
+     * - Entry with UID_RULES_CONFIGURATION_KEY:
+     *    Store the configuration for the current uid rules. It indicates the device
+     *    is in doze/powersave/standby/restricted mode.
+     * - Entry with CURRENT_STATS_MAP_CONFIGURATION_KEY:
+     *    Stores the current live stats map that kernel program is writing to.
+     *    Userspace can do scraping and cleaning job on the other one depending on the
+     *    current configs.
+     */
+    bpf::BpfMap<uint32_t, uint8_t> mConfigurationMap GUARDED_BY(mMutex);
+
+    /*
+     * mUidOwnerMap: Store uids that are used for bandwidth control uid match.
+     */
+    bpf::BpfMap<uint32_t, UidOwnerValue> mUidOwnerMap GUARDED_BY(mMutex);
+
+    /*
+     * mUidOwnerMap: Store uids that are used for INTERNET permission check.
+     */
+    bpf::BpfMap<uint32_t, uint8_t> mUidPermissionMap GUARDED_BY(mMutex);
+
+    std::unique_ptr<netdutils::NetlinkListenerInterface> mSkDestroyListener;
+
+    netdutils::Status removeRule(uint32_t uid, UidOwnerMatchType match) REQUIRES(mMutex);
+
+    netdutils::Status addRule(uint32_t uid, UidOwnerMatchType match, uint32_t iif = 0)
+            REQUIRES(mMutex);
+
+    std::mutex mMutex;
+
+    netdutils::Status initMaps() EXCLUDES(mMutex);
+
+    // Keep track of uids that have permission UPDATE_DEVICE_STATS so we don't
+    // need to call back to system server for permission check.
+    std::set<uid_t> mPrivilegedUser GUARDED_BY(mMutex);
+
+    bool hasUpdateDeviceStatsPermission(uid_t uid) REQUIRES(mMutex);
+
+    // For testing
+    friend class TrafficControllerTest;
+};
+
+}  // namespace net
+}  // namespace android
diff --git a/service/native/jni/com_android_server_BpfNetMaps.cpp b/service/native/jni/com_android_server_BpfNetMaps.cpp
new file mode 100644
index 0000000..7ab4d46
--- /dev/null
+++ b/service/native/jni/com_android_server_BpfNetMaps.cpp
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+#define LOG_TAG "TrafficControllerJni"
+
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedUtfChars.h>
+#include <nativehelper/ScopedPrimitiveArray.h>
+#include <net/if.h>
+#include <vector>
+
+#include "TrafficController.h"
+#include "android-base/logging.h"
+#include "bpf_shared.h"
+#include "utils/Log.h"
+
+using android::net::TrafficController;
+using android::netdutils::Status;
+
+using UidOwnerMatchType::PENALTY_BOX_MATCH;
+using UidOwnerMatchType::HAPPY_BOX_MATCH;
+
+static android::net::TrafficController mTc;
+
+namespace android {
+
+static void native_init(JNIEnv* env, jobject clazz) {
+  Status status = mTc.start();
+   if (!isOk(status)) {
+    ALOGE("%s failed", __func__);
+  }
+}
+
+static jint native_addNaughtyApp(JNIEnv* env, jobject clazz, jint uid) {
+  const uint32_t appUids = static_cast<uint32_t>(abs(uid));
+  Status status = mTc.updateUidOwnerMap(appUids, PENALTY_BOX_MATCH,
+      TrafficController::IptOp::IptOpInsert);
+  if (!isOk(status)) {
+    ALOGE("%s failed, errer code = %d", __func__, status.code());
+  }
+  return (jint)status.code();
+}
+
+static jint native_removeNaughtyApp(JNIEnv* env, jobject clazz, jint uid) {
+  const uint32_t appUids = static_cast<uint32_t>(abs(uid));
+  Status status = mTc.updateUidOwnerMap(appUids, PENALTY_BOX_MATCH,
+      TrafficController::IptOp::IptOpDelete);
+  if (!isOk(status)) {
+    ALOGE("%s failed, errer code = %d", __func__, status.code());
+  }
+  return (jint)status.code();
+}
+
+static jint native_addNiceApp(JNIEnv* env, jobject clazz, jint uid) {
+  const uint32_t appUids = static_cast<uint32_t>(abs(uid));
+  Status status = mTc.updateUidOwnerMap(appUids, HAPPY_BOX_MATCH,
+      TrafficController::IptOp::IptOpInsert);
+  if (!isOk(status)) {
+    ALOGE("%s failed, errer code = %d", __func__, status.code());
+  }
+  return (jint)status.code();
+}
+
+static jint native_removeNiceApp(JNIEnv* env, jobject clazz, jint uid) {
+  const uint32_t appUids = static_cast<uint32_t>(abs(uid));
+  Status status = mTc.updateUidOwnerMap(appUids, HAPPY_BOX_MATCH,
+      TrafficController::IptOp::IptOpDelete);
+  if (!isOk(status)) {
+    ALOGD("%s failed, errer code = %d", __func__, status.code());
+  }
+  return (jint)status.code();
+}
+
+static jint native_setChildChain(JNIEnv* env, jobject clazz, jint childChain, jboolean enable) {
+  auto chain = static_cast<ChildChain>(childChain);
+  int res = mTc.toggleUidOwnerMap(chain, enable);
+  if (res) {
+    ALOGE("%s failed, error code = %d", __func__, res);
+  }
+  return (jint)res;
+}
+
+static jint native_replaceUidChain(JNIEnv* env, jobject clazz, jstring name, jboolean isAllowlist,
+                                jintArray jUids) {
+    const ScopedUtfChars chainNameUtf8(env, name);
+    if (chainNameUtf8.c_str() == nullptr) {
+        return -EINVAL;
+    }
+    const std::string chainName(chainNameUtf8.c_str());
+
+    ScopedIntArrayRO uids(env, jUids);
+    if (uids.get() == nullptr) {
+        return -EINVAL;
+    }
+
+    size_t size = uids.size();
+    std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
+    int res = mTc.replaceUidOwnerMap(chainName, isAllowlist, data);
+    if (res) {
+      ALOGE("%s failed, error code = %d", __func__, res);
+    }
+    return (jint)res;
+}
+
+static FirewallType getFirewallType(ChildChain chain) {
+    switch (chain) {
+        case DOZABLE:
+            return ALLOWLIST;
+        case STANDBY:
+            return DENYLIST;
+        case POWERSAVE:
+            return ALLOWLIST;
+        case RESTRICTED:
+            return ALLOWLIST;
+        case NONE:
+        default:
+            return DENYLIST;
+    }
+}
+
+static jint native_setUidRule(JNIEnv* env, jobject clazz, jint childChain, jint uid,
+                          jint firewallRule) {
+    auto chain = static_cast<ChildChain>(childChain);
+    auto rule = static_cast<FirewallRule>(firewallRule);
+    FirewallType fType = getFirewallType(chain);
+
+    int res = mTc.changeUidOwnerRule(chain, uid, rule, fType);
+    if (res) {
+      ALOGE("%s failed, error code = %d", __func__, res);
+    }
+    return (jint)res;
+}
+
+static jint native_addUidInterfaceRules(JNIEnv* env, jobject clazz, jstring ifName,
+                                    jintArray jUids) {
+    const ScopedUtfChars ifNameUtf8(env, ifName);
+    if (ifNameUtf8.c_str() == nullptr) {
+        return -EINVAL;
+    }
+    const std::string interfaceName(ifNameUtf8.c_str());
+    const int ifIndex = if_nametoindex(interfaceName.c_str());
+
+    ScopedIntArrayRO uids(env, jUids);
+    if (uids.get() == nullptr) {
+        return -EINVAL;
+    }
+
+    size_t size = uids.size();
+    std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
+    Status status = mTc.addUidInterfaceRules(ifIndex, data);
+    if (!isOk(status)) {
+        ALOGE("%s failed, error code = %d", __func__, status.code());
+    }
+    return (jint)status.code();
+}
+
+static jint native_removeUidInterfaceRules(JNIEnv* env, jobject clazz, jintArray jUids) {
+    ScopedIntArrayRO uids(env, jUids);
+    if (uids.get() == nullptr) {
+        return -EINVAL;
+    }
+
+    size_t size = uids.size();
+    std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
+    Status status = mTc.removeUidInterfaceRules(data);
+    if (!isOk(status)) {
+        ALOGE("%s failed, error code = %d", __func__, status.code());
+    }
+    return (jint)status.code();
+}
+
+static jint native_swapActiveStatsMap(JNIEnv* env, jobject clazz) {
+    Status status = mTc.swapActiveStatsMap();
+    if (!isOk(status)) {
+        ALOGD("%s failed, error code = %d", __func__, status.code());
+    }
+    return (jint)status.code();
+}
+
+static void native_setPermissionForUids(JNIEnv* env, jobject clazz, jint permission,
+                                      jintArray jUids) {
+    ScopedIntArrayRO uids(env, jUids);
+    if (uids.get() == nullptr) return;
+
+    size_t size = uids.size();
+    static_assert(sizeof(*(uids.get())) == sizeof(uid_t));
+    std::vector<uid_t> data ((uid_t *)&uids[0], (uid_t*)&uids[size]);
+    mTc.setPermissionForUids(permission, data);
+}
+
+static jint native_setCounterSet(JNIEnv* env, jobject clazz, jint setNum, jint uid) {
+    uid_t callingUid = getuid();
+    int res = mTc.setCounterSet(setNum, (uid_t)uid, callingUid);
+    if (res) {
+      ALOGE("%s failed, error code = %d", __func__, res);
+    }
+    return (jint)res;
+}
+
+static jint native_deleteTagData(JNIEnv* env, jobject clazz, jint tagNum, jint uid) {
+    uid_t callingUid = getuid();
+    int res = mTc.deleteTagData(tagNum, (uid_t)uid, callingUid);
+    if (res) {
+      ALOGE("%s failed, error code = %d", __func__, res);
+    }
+    return (jint)res;
+}
+
+/*
+ * JNI registration.
+ */
+// clang-format off
+static const JNINativeMethod gMethods[] = {
+    /* name, signature, funcPtr */
+    {"native_init", "()V",
+    (void*)native_init},
+    {"native_addNaughtyApp", "(I)I",
+    (void*)native_addNaughtyApp},
+    {"native_removeNaughtyApp", "(I)I",
+    (void*)native_removeNaughtyApp},
+    {"native_addNiceApp", "(I)I",
+    (void*)native_addNiceApp},
+    {"native_removeNiceApp", "(I)I",
+    (void*)native_removeNiceApp},
+    {"native_setChildChain", "(IZ)I",
+    (void*)native_setChildChain},
+    {"native_replaceUidChain", "(Ljava/lang/String;Z[I)I",
+    (void*)native_replaceUidChain},
+    {"native_setUidRule", "(III)I",
+    (void*)native_setUidRule},
+    {"native_addUidInterfaceRules", "(Ljava/lang/String;[I)I",
+    (void*)native_addUidInterfaceRules},
+    {"native_removeUidInterfaceRules", "([I)I",
+    (void*)native_removeUidInterfaceRules},
+    {"native_swapActiveStatsMap", "()I",
+    (void*)native_swapActiveStatsMap},
+    {"native_setPermissionForUids", "(I[I)V",
+    (void*)native_setPermissionForUids},
+    {"native_setCounterSet", "(II)I",
+    (void*)native_setCounterSet},
+    {"native_deleteTagData", "(II)I",
+    (void*)native_deleteTagData},
+};
+// clang-format on
+
+int register_com_android_server_BpfNetMaps(JNIEnv* env) {
+    return jniRegisterNativeMethods(env,
+    "com/android/server/BpfNetMaps",
+    gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/service/native/jni/onload.cpp b/service/native/jni/onload.cpp
new file mode 100644
index 0000000..df7c77b
--- /dev/null
+++ b/service/native/jni/onload.cpp
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+#define LOG_TAG "TrafficControllerJni"
+
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+
+#include "utils/Log.h"
+
+namespace android {
+
+int register_com_android_server_BpfNetMaps(JNIEnv* env);
+
+extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
+    JNIEnv *env;
+    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+        ALOGE("%s: ERROR: GetEnv failed", __func__);
+        return JNI_ERR;
+    }
+
+    if (register_com_android_server_BpfNetMaps(env) < 0)
+      return JNI_ERR;
+
+    return JNI_VERSION_1_6;
+}
+
+}; // namespace android
diff --git a/service/native/libs/libclat/Android.bp b/service/native/libs/libclat/Android.bp
new file mode 100644
index 0000000..5e208d8
--- /dev/null
+++ b/service/native/libs/libclat/Android.bp
@@ -0,0 +1,70 @@
+// Copyright (C) 2022 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_static {
+    name: "libclat",
+    defaults: ["netd_defaults"],
+    header_libs: [
+        "bpf_connectivity_headers",
+        "bpf_headers",
+        "bpf_syscall_wrappers",
+    ],
+    srcs: [
+        "TcUtils.cpp",  // TODO: move to frameworks/libs/net
+        "bpfhelper.cpp",
+        "clatutils.cpp",
+    ],
+    stl: "libc++_static",
+    static_libs: [
+        "libbase",
+        "libip_checksum",
+        "libnetdutils",  // for netdutils/UidConstants.h in bpf_shared.h
+    ],
+    shared_libs: ["liblog"],
+    export_include_dirs: ["include"],
+    min_sdk_version: "30",
+    apex_available: ["com.android.tethering"],
+}
+
+cc_test {
+    name: "libclat_test",
+    defaults: ["netd_defaults"],
+    test_suites: ["device-tests"],
+    header_libs: [
+        "bpf_connectivity_headers",
+        "bpf_headers",
+        "bpf_syscall_wrappers",
+    ],
+    srcs: [
+        "TcUtilsTest.cpp",
+        "clatutils_test.cpp",
+    ],
+    static_libs: [
+        "libbase",
+        "libclat",
+        "libip_checksum",
+        "libnetd_test_tun_interface",
+        "libnetdutils",  // for netdutils/UidConstants.h in bpf_shared.h
+        "libtcutils",
+    ],
+    shared_libs: [
+        "liblog",
+        "libnetutils",
+    ],
+    require_root: true,
+}
diff --git a/service/native/libs/libclat/TcUtils.cpp b/service/native/libs/libclat/TcUtils.cpp
new file mode 100644
index 0000000..cdfb763
--- /dev/null
+++ b/service/native/libs/libclat/TcUtils.cpp
@@ -0,0 +1,390 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+#define LOG_TAG "TcUtils"
+
+#include "libclat/TcUtils.h"
+
+#include <arpa/inet.h>
+#include <linux/if.h>
+#include <linux/if_arp.h>
+#include <linux/netlink.h>
+#include <linux/pkt_cls.h>
+#include <linux/pkt_sched.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <log/log.h>
+
+#include "android-base/unique_fd.h"
+
+namespace android {
+namespace net {
+
+using std::max;
+
+// Sync from system/netd/server/NetlinkCommands.h
+const sockaddr_nl KERNEL_NLADDR = {AF_NETLINK, 0, 0, 0};
+const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK;
+
+static int doSIOCGIF(const std::string& interface, int opt) {
+    base::unique_fd ufd(socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0));
+
+    if (ufd < 0) {
+        const int err = errno;
+        ALOGE("socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0)");
+        return -err;
+    };
+
+    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.c_str(), sizeof(ifr.ifr_name));
+
+    if (ioctl(ufd, opt, &ifr, sizeof(ifr))) return -errno;
+
+    if (opt == SIOCGIFHWADDR) return ifr.ifr_hwaddr.sa_family;
+    if (opt == SIOCGIFMTU) return ifr.ifr_mtu;
+    return -EINVAL;
+}
+
+int hardwareAddressType(const std::string& interface) {
+    return doSIOCGIF(interface, SIOCGIFHWADDR);
+}
+
+int deviceMTU(const std::string& interface) {
+    return doSIOCGIF(interface, SIOCGIFMTU);
+}
+
+base::Result<bool> isEthernet(const std::string& interface) {
+    int rv = hardwareAddressType(interface);
+    if (rv < 0) {
+        errno = -rv;
+        return ErrnoErrorf("Get hardware address type of interface {} failed", interface);
+    }
+
+    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:
+            errno = EAFNOSUPPORT;  // Address family not supported
+            return ErrnoErrorf("Unknown hardware address type {} on interface {}", rv, interface);
+    }
+}
+
+// TODO: use //system/netd/server/NetlinkCommands.cpp:openNetlinkSocket(protocol)
+// and //system/netd/server/SockDiag.cpp:checkError(fd)
+static int sendAndProcessNetlinkResponse(const void* req, int len) {
+    base::unique_fd fd(socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE));
+    if (fd == -1) {
+        const int err = errno;
+        ALOGE("socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE)");
+        return -err;
+    }
+
+    static constexpr int on = 1;
+    int rv = setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &on, sizeof(on));
+    if (rv) ALOGE("setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, %d)", on);
+
+    // this is needed to get sane strace netlink parsing, it allocates the pid
+    rv = bind(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR));
+    if (rv) {
+        const int err = errno;
+        ALOGE("bind(fd, {AF_NETLINK, 0, 0})");
+        return -err;
+    }
+
+    // we do not want to receive messages from anyone besides the kernel
+    rv = connect(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR));
+    if (rv) {
+        const int err = errno;
+        ALOGE("connect(fd, {AF_NETLINK, 0, 0})");
+        return -err;
+    }
+
+    rv = send(fd, req, len, 0);
+    if (rv == -1) return -errno;
+    if (rv != len) return -EMSGSIZE;
+
+    struct {
+        nlmsghdr h;
+        nlmsgerr e;
+        char buf[256];
+    } resp = {};
+
+    rv = recv(fd, &resp, sizeof(resp), MSG_TRUNC);
+
+    if (rv == -1) {
+        const int err = errno;
+        ALOGE("recv() failed");
+        return -err;
+    }
+
+    if (rv < (int)NLMSG_SPACE(sizeof(struct nlmsgerr))) {
+        ALOGE("recv() returned short packet: %d", rv);
+        return -EMSGSIZE;
+    }
+
+    if (resp.h.nlmsg_len != (unsigned)rv) {
+        ALOGE("recv() returned invalid header length: %d != %d", resp.h.nlmsg_len, rv);
+        return -EBADMSG;
+    }
+
+    if (resp.h.nlmsg_type != NLMSG_ERROR) {
+        ALOGE("recv() did not return NLMSG_ERROR message: %d", resp.h.nlmsg_type);
+        return -EBADMSG;
+    }
+
+    return resp.e.error;  // returns 0 on success
+}
+
+// ADD:     nlMsgType=RTM_NEWQDISC nlMsgFlags=NLM_F_EXCL|NLM_F_CREATE
+// REPLACE: nlMsgType=RTM_NEWQDISC nlMsgFlags=NLM_F_CREATE|NLM_F_REPLACE
+// DEL:     nlMsgType=RTM_DELQDISC nlMsgFlags=0
+int doTcQdiscClsact(int ifIndex, uint16_t nlMsgType, uint16_t nlMsgFlags) {
+    // This is the name of the qdisc we are attaching.
+    // Some hoop jumping to make this compile time constant with known size,
+    // so that the structure declaration is well defined at compile time.
+#define CLSACT "clsact"
+    // sizeof() includes the terminating NULL
+    static constexpr size_t ASCIIZ_LEN_CLSACT = sizeof(CLSACT);
+
+    const struct {
+        nlmsghdr n;
+        tcmsg t;
+        struct {
+            nlattr attr;
+            char str[NLMSG_ALIGN(ASCIIZ_LEN_CLSACT)];
+        } kind;
+    } req = {
+            .n =
+                    {
+                            .nlmsg_len = sizeof(req),
+                            .nlmsg_type = nlMsgType,
+                            .nlmsg_flags = static_cast<__u16>(NETLINK_REQUEST_FLAGS | nlMsgFlags),
+                    },
+            .t =
+                    {
+                            .tcm_family = AF_UNSPEC,
+                            .tcm_ifindex = ifIndex,
+                            .tcm_handle = TC_H_MAKE(TC_H_CLSACT, 0),
+                            .tcm_parent = TC_H_CLSACT,
+                    },
+            .kind =
+                    {
+                            .attr =
+                                    {
+                                            .nla_len = NLA_HDRLEN + ASCIIZ_LEN_CLSACT,
+                                            .nla_type = TCA_KIND,
+                                    },
+                            .str = CLSACT,
+                    },
+    };
+#undef CLSACT
+
+    return sendAndProcessNetlinkResponse(&req, sizeof(req));
+}
+
+// tc filter add dev .. in/egress prio 4 protocol ipv6/ip bpf object-pinned /sys/fs/bpf/...
+// direct-action
+int tcFilterAddDevBpf(int ifIndex, bool ingress, uint16_t proto, int bpfFd, bool ethernet) {
+    // This is the name of the filter we're attaching (ie. this is the 'bpf'
+    // packet classifier enabled by kernel config option CONFIG_NET_CLS_BPF.
+    //
+    // We go through some hoops in order to make this compile time constants
+    // so that we can define the struct further down the function with the
+    // field for this sized correctly already during the build.
+#define BPF "bpf"
+    // sizeof() includes the terminating NULL
+    static constexpr size_t ASCIIZ_LEN_BPF = sizeof(BPF);
+
+    // This is to replicate program name suffix used by 'tc' Linux cli
+    // when it attaches programs.
+#define FSOBJ_SUFFIX ":[*fsobj]"
+
+    // This macro expands (from header files) to:
+    //   prog_clatd_schedcls_ingress6_clat_rawip:[*fsobj]
+    // and is the name of the pinned ingress ebpf program for ARPHRD_RAWIP interfaces.
+    // (also compatible with anything that has 0 size L2 header)
+    static constexpr char name_clat_rx_rawip[] = CLAT_INGRESS6_PROG_RAWIP_NAME FSOBJ_SUFFIX;
+
+    // This macro expands (from header files) to:
+    //   prog_clatd_schedcls_ingress6_clat_ether:[*fsobj]
+    // and is the name of the pinned ingress ebpf program for ARPHRD_ETHER interfaces.
+    // (also compatible with anything that has standard ethernet header)
+    static constexpr char name_clat_rx_ether[] = CLAT_INGRESS6_PROG_ETHER_NAME FSOBJ_SUFFIX;
+
+    // This macro expands (from header files) to:
+    //   prog_clatd_schedcls_egress4_clat_rawip:[*fsobj]
+    // and is the name of the pinned egress ebpf program for ARPHRD_RAWIP interfaces.
+    // (also compatible with anything that has 0 size L2 header)
+    static constexpr char name_clat_tx_rawip[] = CLAT_EGRESS4_PROG_RAWIP_NAME FSOBJ_SUFFIX;
+
+    // This macro expands (from header files) to:
+    //   prog_clatd_schedcls_egress4_clat_ether:[*fsobj]
+    // and is the name of the pinned egress ebpf program for ARPHRD_ETHER interfaces.
+    // (also compatible with anything that has standard ethernet header)
+    static constexpr char name_clat_tx_ether[] = CLAT_EGRESS4_PROG_ETHER_NAME FSOBJ_SUFFIX;
+
+#undef FSOBJ_SUFFIX
+
+    // The actual name we'll use is determined at run time via 'ethernet' and 'ingress'
+    // booleans.  We need to compile time allocate enough space in the struct
+    // hence this macro magic to make sure we have enough space for either
+    // possibility.  In practice some of these are actually the same size.
+    static constexpr size_t ASCIIZ_MAXLEN_NAME = max({
+            sizeof(name_clat_rx_rawip),
+            sizeof(name_clat_rx_ether),
+            sizeof(name_clat_tx_rawip),
+            sizeof(name_clat_tx_ether),
+    });
+
+    // These are not compile time constants: 'name' is used in strncpy below
+    const char* const name_clat_rx = ethernet ? name_clat_rx_ether : name_clat_rx_rawip;
+    const char* const name_clat_tx = ethernet ? name_clat_tx_ether : name_clat_tx_rawip;
+    const char* const name = ingress ? name_clat_rx : name_clat_tx;
+
+    struct {
+        nlmsghdr n;
+        tcmsg t;
+        struct {
+            nlattr attr;
+            char str[NLMSG_ALIGN(ASCIIZ_LEN_BPF)];
+        } kind;
+        struct {
+            nlattr attr;
+            struct {
+                nlattr attr;
+                __u32 u32;
+            } fd;
+            struct {
+                nlattr attr;
+                char str[NLMSG_ALIGN(ASCIIZ_MAXLEN_NAME)];
+            } 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>((PRIO_CLAT << 16) | htons(proto)),
+                    },
+            .kind =
+                    {
+                            .attr =
+                                    {
+                                            .nla_len = sizeof(req.kind),
+                                            .nla_type = TCA_KIND,
+                                    },
+                            .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,
+                                    },
+                    },
+    };
+#undef BPF
+
+    strncpy(req.options.name.str, name, sizeof(req.options.name.str));
+
+    return sendAndProcessNetlinkResponse(&req, sizeof(req));
+}
+
+// tc filter del dev .. in/egress prio 4 protocol ..
+int tcFilterDelDev(int ifIndex, bool ingress, uint16_t prio, uint16_t 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<uint32_t>(prio) << 16) |
+                                        static_cast<uint32_t>(htons(proto)),
+                    },
+    };
+
+    return sendAndProcessNetlinkResponse(&req, sizeof(req));
+}
+
+}  // namespace net
+}  // namespace android
diff --git a/service/native/libs/libclat/TcUtilsTest.cpp b/service/native/libs/libclat/TcUtilsTest.cpp
new file mode 100644
index 0000000..08f3042
--- /dev/null
+++ b/service/native/libs/libclat/TcUtilsTest.cpp
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2019 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.
+ *
+ * TcUtilsTest.cpp - unit tests for TcUtils.cpp
+ */
+
+#include <gtest/gtest.h>
+
+#include "libclat/TcUtils.h"
+
+#include <linux/if_arp.h>
+#include <stdlib.h>
+#include <sys/wait.h>
+
+#include "bpf/BpfUtils.h"
+#include "bpf_shared.h"
+
+namespace android {
+namespace net {
+
+class TcUtilsTest : public ::testing::Test {
+  public:
+    void SetUp() {}
+};
+
+TEST_F(TcUtilsTest, HardwareAddressTypeOfNonExistingIf) {
+    ASSERT_EQ(-ENODEV, hardwareAddressType("not_existing_if"));
+}
+
+TEST_F(TcUtilsTest, HardwareAddressTypeOfLoopback) {
+    ASSERT_EQ(ARPHRD_LOOPBACK, hardwareAddressType("lo"));
+}
+
+// If wireless 'wlan0' interface exists it should be Ethernet.
+TEST_F(TcUtilsTest, HardwareAddressTypeOfWireless) {
+    int type = hardwareAddressType("wlan0");
+    if (type == -ENODEV) return;
+
+    ASSERT_EQ(ARPHRD_ETHER, type);
+}
+
+// If cellular 'rmnet_data0' interface exists it should
+// *probably* not be Ethernet and instead be RawIp.
+TEST_F(TcUtilsTest, HardwareAddressTypeOfCellular) {
+    int type = hardwareAddressType("rmnet_data0");
+    if (type == -ENODEV) return;
+
+    ASSERT_NE(ARPHRD_ETHER, type);
+
+    // ARPHRD_RAWIP is 530 on some pre-4.14 Qualcomm devices.
+    if (type == 530) return;
+
+    ASSERT_EQ(ARPHRD_RAWIP, type);
+}
+
+TEST_F(TcUtilsTest, IsEthernetOfNonExistingIf) {
+    auto res = isEthernet("not_existing_if");
+    ASSERT_FALSE(res.ok());
+    ASSERT_EQ(ENODEV, res.error().code());
+}
+
+TEST_F(TcUtilsTest, IsEthernetOfLoopback) {
+    auto res = isEthernet("lo");
+    ASSERT_FALSE(res.ok());
+    ASSERT_EQ(EAFNOSUPPORT, res.error().code());
+}
+
+// If wireless 'wlan0' interface exists it should be Ethernet.
+// See also HardwareAddressTypeOfWireless.
+TEST_F(TcUtilsTest, IsEthernetOfWireless) {
+    auto res = isEthernet("wlan0");
+    if (!res.ok() && res.error().code() == ENODEV) return;
+
+    ASSERT_RESULT_OK(res);
+    ASSERT_TRUE(res.value());
+}
+
+// If cellular 'rmnet_data0' interface exists it should
+// *probably* not be Ethernet and instead be RawIp.
+// See also HardwareAddressTypeOfCellular.
+TEST_F(TcUtilsTest, IsEthernetOfCellular) {
+    auto res = isEthernet("rmnet_data0");
+    if (!res.ok() && res.error().code() == ENODEV) return;
+
+    ASSERT_RESULT_OK(res);
+    ASSERT_FALSE(res.value());
+}
+
+TEST_F(TcUtilsTest, DeviceMTUOfNonExistingIf) {
+    ASSERT_EQ(-ENODEV, deviceMTU("not_existing_if"));
+}
+
+TEST_F(TcUtilsTest, DeviceMTUofLoopback) {
+    ASSERT_EQ(65536, deviceMTU("lo"));
+}
+
+TEST_F(TcUtilsTest, GetClatEgress4MapFd) {
+    int fd = getClatEgress4MapFd();
+    ASSERT_GE(fd, 3);  // 0,1,2 - stdin/out/err, thus fd >= 3
+    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
+    close(fd);
+}
+
+TEST_F(TcUtilsTest, GetClatEgress4RawIpProgFd) {
+    int fd = getClatEgress4ProgFd(RAWIP);
+    ASSERT_GE(fd, 3);
+    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
+    close(fd);
+}
+
+TEST_F(TcUtilsTest, GetClatEgress4EtherProgFd) {
+    int fd = getClatEgress4ProgFd(ETHER);
+    ASSERT_GE(fd, 3);
+    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
+    close(fd);
+}
+
+TEST_F(TcUtilsTest, GetClatIngress6MapFd) {
+    int fd = getClatIngress6MapFd();
+    ASSERT_GE(fd, 3);  // 0,1,2 - stdin/out/err, thus fd >= 3
+    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
+    close(fd);
+}
+
+TEST_F(TcUtilsTest, GetClatIngress6RawIpProgFd) {
+    int fd = getClatIngress6ProgFd(RAWIP);
+    ASSERT_GE(fd, 3);
+    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
+    close(fd);
+}
+
+TEST_F(TcUtilsTest, GetClatIngress6EtherProgFd) {
+    int fd = getClatIngress6ProgFd(ETHER);
+    ASSERT_GE(fd, 3);
+    EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
+    close(fd);
+}
+
+// See Linux kernel source in include/net/flow.h
+#define LOOPBACK_IFINDEX 1
+
+TEST_F(TcUtilsTest, AttachReplaceDetachClsactLo) {
+    // This attaches and detaches a configuration-less and thus no-op clsact
+    // qdisc to loopback interface (and it takes fractions of a second)
+    EXPECT_EQ(0, tcQdiscAddDevClsact(LOOPBACK_IFINDEX));
+    EXPECT_EQ(0, tcQdiscReplaceDevClsact(LOOPBACK_IFINDEX));
+    EXPECT_EQ(0, tcQdiscDelDevClsact(LOOPBACK_IFINDEX));
+    EXPECT_EQ(-EINVAL, tcQdiscDelDevClsact(LOOPBACK_IFINDEX));
+}
+
+static void checkAttachDetachBpfFilterClsactLo(const bool ingress, const bool ethernet) {
+    // Older kernels return EINVAL instead of ENOENT due to lacking proper error propagation...
+    const int errNOENT = android::bpf::isAtLeastKernelVersion(4, 19, 0) ? ENOENT : EINVAL;
+
+    int clatBpfFd = ingress ? getClatIngress6ProgFd(ethernet) : getClatEgress4ProgFd(ethernet);
+    ASSERT_GE(clatBpfFd, 3);
+
+    // This attaches and detaches a clsact plus ebpf program to loopback
+    // interface, but it should not affect traffic by virtue of us not
+    // actually populating the ebpf control map.
+    // Furthermore: it only takes fractions of a second.
+    EXPECT_EQ(-EINVAL, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
+    EXPECT_EQ(-EINVAL, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
+    EXPECT_EQ(0, tcQdiscAddDevClsact(LOOPBACK_IFINDEX));
+    EXPECT_EQ(-errNOENT, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
+    EXPECT_EQ(-errNOENT, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
+    if (ingress) {
+        EXPECT_EQ(0, tcFilterAddDevIngressClatIpv6(LOOPBACK_IFINDEX, clatBpfFd, ethernet));
+        EXPECT_EQ(0, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
+    } else {
+        EXPECT_EQ(0, tcFilterAddDevEgressClatIpv4(LOOPBACK_IFINDEX, clatBpfFd, ethernet));
+        EXPECT_EQ(0, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
+    }
+    EXPECT_EQ(-errNOENT, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
+    EXPECT_EQ(-errNOENT, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
+    EXPECT_EQ(0, tcQdiscDelDevClsact(LOOPBACK_IFINDEX));
+    EXPECT_EQ(-EINVAL, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
+    EXPECT_EQ(-EINVAL, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
+
+    close(clatBpfFd);
+}
+
+TEST_F(TcUtilsTest, CheckAttachBpfFilterRawIpClsactEgressLo) {
+    checkAttachDetachBpfFilterClsactLo(EGRESS, RAWIP);
+}
+
+TEST_F(TcUtilsTest, CheckAttachBpfFilterEthernetClsactEgressLo) {
+    checkAttachDetachBpfFilterClsactLo(EGRESS, ETHER);
+}
+
+TEST_F(TcUtilsTest, CheckAttachBpfFilterRawIpClsactIngressLo) {
+    checkAttachDetachBpfFilterClsactLo(INGRESS, RAWIP);
+}
+
+TEST_F(TcUtilsTest, CheckAttachBpfFilterEthernetClsactIngressLo) {
+    checkAttachDetachBpfFilterClsactLo(INGRESS, ETHER);
+}
+
+}  // namespace net
+}  // namespace android
diff --git a/service/native/libs/libclat/bpfhelper.cpp b/service/native/libs/libclat/bpfhelper.cpp
new file mode 100644
index 0000000..6e230d0
--- /dev/null
+++ b/service/native/libs/libclat/bpfhelper.cpp
@@ -0,0 +1,231 @@
+/*
+ * Copyright 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.
+ *
+ * main.c - main function
+ */
+#define LOG_TAG "bpfhelper"
+
+#include "libclat/bpfhelper.h"
+
+#include <android-base/unique_fd.h>
+#include <log/log.h>
+
+#include "bpf/BpfMap.h"
+#include "libclat/TcUtils.h"
+
+#define DEVICEPREFIX "v4-"
+
+using android::base::unique_fd;
+using android::net::RAWIP;
+using android::net::getClatEgress4MapFd;
+using android::net::getClatIngress6MapFd;
+using android::net::getClatEgress4ProgFd;
+using android::net::getClatIngress6ProgFd;
+using android::net::tcQdiscAddDevClsact;
+using android::net::tcFilterAddDevEgressClatIpv4;
+using android::net::tcFilterAddDevIngressClatIpv6;
+using android::net::tcFilterDelDevEgressClatIpv4;
+using android::net::tcFilterDelDevIngressClatIpv6;
+using android::bpf::BpfMap;
+
+BpfMap<ClatEgress4Key, ClatEgress4Value> mClatEgress4Map;
+BpfMap<ClatIngress6Key, ClatIngress6Value> mClatIngress6Map;
+
+namespace android {
+namespace net {
+namespace clat {
+
+// TODO: have a clearMap function to remove all stubs while system server crash.
+// For long term, move bpf access into java and map initialization should live
+// ClatCoordinator constructor.
+int initMaps(void) {
+    int rv = getClatEgress4MapFd();
+    if (rv < 0) {
+        ALOGE("getClatEgress4MapFd() failure: %s", strerror(-rv));
+        return -rv;
+    }
+    mClatEgress4Map.reset(rv);
+
+    rv = getClatIngress6MapFd();
+    if (rv < 0) {
+        ALOGE("getClatIngress6MapFd() failure: %s", strerror(-rv));
+        mClatEgress4Map.reset(-1);
+        return -rv;
+    }
+    mClatIngress6Map.reset(rv);
+
+    return 0;
+}
+
+void maybeStartBpf(const ClatdTracker& tracker) {
+    auto isEthernet = android::net::isEthernet(tracker.iface);
+    if (!isEthernet.ok()) {
+        ALOGE("isEthernet(%s[%d]) failure: %s", tracker.iface, tracker.ifIndex,
+              isEthernet.error().message().c_str());
+        return;
+    }
+
+    // This program will be attached to the v4-* interface which is a TUN and thus always rawip.
+    int rv = getClatEgress4ProgFd(RAWIP);
+    if (rv < 0) {
+        ALOGE("getClatEgress4ProgFd(RAWIP) failure: %s", strerror(-rv));
+        return;
+    }
+    unique_fd txRawIpProgFd(rv);
+
+    rv = getClatIngress6ProgFd(isEthernet.value());
+    if (rv < 0) {
+        ALOGE("getClatIngress6ProgFd(%d) failure: %s", isEthernet.value(), strerror(-rv));
+        return;
+    }
+    unique_fd rxProgFd(rv);
+
+    ClatEgress4Key txKey = {
+            .iif = tracker.v4ifIndex,
+            .local4 = tracker.v4,
+    };
+    ClatEgress4Value txValue = {
+            .oif = tracker.ifIndex,
+            .local6 = tracker.v6,
+            .pfx96 = tracker.pfx96,
+            .oifIsEthernet = isEthernet.value(),
+    };
+
+    auto ret = mClatEgress4Map.writeValue(txKey, txValue, BPF_ANY);
+    if (!ret.ok()) {
+        ALOGE("mClatEgress4Map.writeValue failure: %s", strerror(ret.error().code()));
+        return;
+    }
+
+    ClatIngress6Key rxKey = {
+            .iif = tracker.ifIndex,
+            .pfx96 = tracker.pfx96,
+            .local6 = tracker.v6,
+    };
+    ClatIngress6Value rxValue = {
+            // TODO: move all the clat code to eBPF and remove the tun interface entirely.
+            .oif = tracker.v4ifIndex,
+            .local4 = tracker.v4,
+    };
+
+    ret = mClatIngress6Map.writeValue(rxKey, rxValue, BPF_ANY);
+    if (!ret.ok()) {
+        ALOGE("mClatIngress6Map.writeValue failure: %s", strerror(ret.error().code()));
+        ret = mClatEgress4Map.deleteValue(txKey);
+        if (!ret.ok())
+            ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
+        return;
+    }
+
+    // We do tc setup *after* populating the maps, so scanning through them
+    // can always be used to tell us what needs cleanup.
+
+    // Usually the clsact will be added in RouteController::addInterfaceToPhysicalNetwork.
+    // But clat is started before the v4- interface is added to the network. The clat startup have
+    // to add clsact of v4- tun interface first for adding bpf filter in maybeStartBpf.
+    // TODO: move "qdisc add clsact" of v4- tun interface out from ClatdController.
+    rv = tcQdiscAddDevClsact(tracker.v4ifIndex);
+    if (rv) {
+        ALOGE("tcQdiscAddDevClsact(%d[%s]) failure: %s", tracker.v4ifIndex, tracker.v4iface,
+              strerror(-rv));
+        ret = mClatEgress4Map.deleteValue(txKey);
+        if (!ret.ok())
+            ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
+        ret = mClatIngress6Map.deleteValue(rxKey);
+        if (!ret.ok())
+            ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code()));
+        return;
+    }
+
+    rv = tcFilterAddDevEgressClatIpv4(tracker.v4ifIndex, txRawIpProgFd, RAWIP);
+    if (rv) {
+        ALOGE("tcFilterAddDevEgressClatIpv4(%d[%s], RAWIP) failure: %s", tracker.v4ifIndex,
+              tracker.v4iface, strerror(-rv));
+
+        // The v4- interface clsact is not deleted for unwinding error because once it is created
+        // with interface addition, the lifetime is till interface deletion. Moreover, the clsact
+        // has no clat filter now. It should not break anything.
+
+        ret = mClatEgress4Map.deleteValue(txKey);
+        if (!ret.ok())
+            ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
+        ret = mClatIngress6Map.deleteValue(rxKey);
+        if (!ret.ok())
+            ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code()));
+        return;
+    }
+
+    rv = tcFilterAddDevIngressClatIpv6(tracker.ifIndex, rxProgFd, isEthernet.value());
+    if (rv) {
+        ALOGE("tcFilterAddDevIngressClatIpv6(%d[%s], %d) failure: %s", tracker.ifIndex,
+              tracker.iface, isEthernet.value(), strerror(-rv));
+        rv = tcFilterDelDevEgressClatIpv4(tracker.v4ifIndex);
+        if (rv) {
+            ALOGE("tcFilterDelDevEgressClatIpv4(%d[%s]) failure: %s", tracker.v4ifIndex,
+                  tracker.v4iface, strerror(-rv));
+        }
+
+        // The v4- interface clsact is not deleted. See the reason in the error unwinding code of
+        // the egress filter attaching of v4- tun interface.
+
+        ret = mClatEgress4Map.deleteValue(txKey);
+        if (!ret.ok())
+            ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
+        ret = mClatIngress6Map.deleteValue(rxKey);
+        if (!ret.ok())
+            ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code()));
+        return;
+    }
+
+    // success
+}
+
+void maybeStopBpf(const ClatdTracker& tracker) {
+    int rv = tcFilterDelDevIngressClatIpv6(tracker.ifIndex);
+    if (rv < 0) {
+        ALOGE("tcFilterDelDevIngressClatIpv6(%d[%s]) failure: %s", tracker.ifIndex, tracker.iface,
+              strerror(-rv));
+    }
+
+    rv = tcFilterDelDevEgressClatIpv4(tracker.v4ifIndex);
+    if (rv < 0) {
+        ALOGE("tcFilterDelDevEgressClatIpv4(%d[%s]) failure: %s", tracker.v4ifIndex,
+              tracker.v4iface, strerror(-rv));
+    }
+
+    // We cleanup the maps last, so scanning through them can be used to
+    // determine what still needs cleanup.
+
+    ClatEgress4Key txKey = {
+            .iif = tracker.v4ifIndex,
+            .local4 = tracker.v4,
+    };
+
+    auto ret = mClatEgress4Map.deleteValue(txKey);
+    if (!ret.ok()) ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
+
+    ClatIngress6Key rxKey = {
+            .iif = tracker.ifIndex,
+            .pfx96 = tracker.pfx96,
+            .local6 = tracker.v6,
+    };
+
+    ret = mClatIngress6Map.deleteValue(rxKey);
+    if (!ret.ok()) ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code()));
+}
+
+}  // namespace clat
+}  // namespace net
+}  // namespace android
diff --git a/service/native/libs/libclat/clatutils.cpp b/service/native/libs/libclat/clatutils.cpp
new file mode 100644
index 0000000..4a125ba
--- /dev/null
+++ b/service/native/libs/libclat/clatutils.cpp
@@ -0,0 +1,268 @@
+// Copyright (C) 2022 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.
+
+#define LOG_TAG "clatutils"
+
+#include "libclat/clatutils.h"
+
+#include <errno.h>
+#include <linux/filter.h>
+#include <linux/if_packet.h>
+#include <linux/if_tun.h>
+#include <log/log.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+extern "C" {
+#include "checksum.h"
+}
+
+// Sync from external/android-clat/clatd.h
+#define MAXMTU 65536
+#define PACKETLEN (MAXMTU + sizeof(struct tun_pi))
+
+// Sync from system/netd/include/netid_client.h.
+#define MARK_UNSET 0u
+
+namespace android {
+namespace net {
+namespace clat {
+
+bool isIpv4AddressFree(in_addr_t addr) {
+    int s = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+    if (s == -1) {
+        return 0;
+    }
+
+    // Attempt to connect to the address. If the connection succeeds and getsockname returns the
+    // same then the address is already assigned to the system and we can't use it.
+    struct sockaddr_in sin = {
+            .sin_family = AF_INET,
+            .sin_port = htons(53),
+            .sin_addr = {addr},
+    };
+    socklen_t len = sizeof(sin);
+    bool inuse = connect(s, (struct sockaddr*)&sin, sizeof(sin)) == 0 &&
+                 getsockname(s, (struct sockaddr*)&sin, &len) == 0 && (size_t)len >= sizeof(sin) &&
+                 sin.sin_addr.s_addr == addr;
+
+    close(s);
+    return !inuse;
+}
+
+// Picks a free IPv4 address, starting from ip and trying all addresses in the prefix in order.
+//   ip        - the IP address from the configuration file
+//   prefixlen - the length of the prefix from which addresses may be selected.
+//   returns: the IPv4 address, or INADDR_NONE if no addresses were available
+in_addr_t selectIpv4Address(const in_addr ip, int16_t prefixlen) {
+    return selectIpv4AddressInternal(ip, prefixlen, isIpv4AddressFree);
+}
+
+// Only allow testing to use this function directly. Otherwise call selectIpv4Address(ip, pfxlen)
+// which has applied valid isIpv4AddressFree function pointer.
+in_addr_t selectIpv4AddressInternal(const in_addr ip, int16_t prefixlen,
+                                    isIpv4AddrFreeFn isIpv4AddressFreeFunc) {
+    // Impossible! Only test allows to apply fn.
+    if (isIpv4AddressFreeFunc == nullptr) {
+        return INADDR_NONE;
+    }
+
+    // Don't accept prefixes that are too large because we scan addresses one by one.
+    if (prefixlen < 16 || prefixlen > 32) {
+        return INADDR_NONE;
+    }
+
+    // All these are in host byte order.
+    in_addr_t mask = 0xffffffff >> (32 - prefixlen) << (32 - prefixlen);
+    in_addr_t ipv4 = ntohl(ip.s_addr);
+    in_addr_t first_ipv4 = ipv4;
+    in_addr_t prefix = ipv4 & mask;
+
+    // Pick the first IPv4 address in the pool, wrapping around if necessary.
+    // So, for example, 192.0.0.4 -> 192.0.0.5 -> 192.0.0.6 -> 192.0.0.7 -> 192.0.0.0.
+    do {
+        if (isIpv4AddressFreeFunc(htonl(ipv4))) {
+            return htonl(ipv4);
+        }
+        ipv4 = prefix | ((ipv4 + 1) & ~mask);
+    } while (ipv4 != first_ipv4);
+
+    return INADDR_NONE;
+}
+
+// Alters the bits in the IPv6 address to make them checksum neutral with v4 and nat64Prefix.
+void makeChecksumNeutral(in6_addr* v6, const in_addr v4, const in6_addr& nat64Prefix) {
+    // Fill last 8 bytes of IPv6 address with random bits.
+    arc4random_buf(&v6->s6_addr[8], 8);
+
+    // Make the IID checksum-neutral. That is, make it so that:
+    //   checksum(Local IPv4 | Remote IPv4) = checksum(Local IPv6 | Remote IPv6)
+    // in other words (because remote IPv6 = NAT64 prefix | Remote IPv4):
+    //   checksum(Local IPv4) = checksum(Local IPv6 | NAT64 prefix)
+    // Do this by adjusting the two bytes in the middle of the IID.
+
+    uint16_t middlebytes = (v6->s6_addr[11] << 8) + v6->s6_addr[12];
+
+    uint32_t c1 = ip_checksum_add(0, &v4, sizeof(v4));
+    uint32_t c2 = ip_checksum_add(0, &nat64Prefix, sizeof(nat64Prefix)) +
+                  ip_checksum_add(0, v6, sizeof(*v6));
+
+    uint16_t delta = ip_checksum_adjust(middlebytes, c1, c2);
+    v6->s6_addr[11] = delta >> 8;
+    v6->s6_addr[12] = delta & 0xff;
+}
+
+// Picks a random interface ID that is checksum neutral with the IPv4 address and the NAT64 prefix.
+int generateIpv6Address(const char* iface, const in_addr v4, const in6_addr& nat64Prefix,
+                        in6_addr* v6) {
+    int s = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+    if (s == -1) return -errno;
+
+    if (setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, iface, strlen(iface) + 1) == -1) {
+        close(s);
+        return -errno;
+    }
+
+    sockaddr_in6 sin6 = {.sin6_family = AF_INET6, .sin6_addr = nat64Prefix};
+    if (connect(s, reinterpret_cast<struct sockaddr*>(&sin6), sizeof(sin6)) == -1) {
+        close(s);
+        return -errno;
+    }
+
+    socklen_t len = sizeof(sin6);
+    if (getsockname(s, reinterpret_cast<struct sockaddr*>(&sin6), &len) == -1) {
+        close(s);
+        return -errno;
+    }
+
+    *v6 = sin6.sin6_addr;
+
+    if (IN6_IS_ADDR_UNSPECIFIED(v6) || IN6_IS_ADDR_LOOPBACK(v6) || IN6_IS_ADDR_LINKLOCAL(v6) ||
+        IN6_IS_ADDR_SITELOCAL(v6) || IN6_IS_ADDR_ULA(v6)) {
+        close(s);
+        return -ENETUNREACH;
+    }
+
+    makeChecksumNeutral(v6, v4, nat64Prefix);
+    close(s);
+
+    return 0;
+}
+
+int detect_mtu(const struct in6_addr* plat_subnet, uint32_t plat_suffix, uint32_t mark) {
+    // Create an IPv6 UDP socket.
+    int s = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+    if (s < 0) {
+        int ret = errno;
+        ALOGE("socket(AF_INET6, SOCK_DGRAM, 0) failed: %s", strerror(errno));
+        return -ret;
+    }
+
+    // Socket's mark affects routing decisions (network selection)
+    if ((mark != MARK_UNSET) && setsockopt(s, SOL_SOCKET, SO_MARK, &mark, sizeof(mark))) {
+        int ret = errno;
+        ALOGE("setsockopt(SOL_SOCKET, SO_MARK) failed: %s", strerror(errno));
+        close(s);
+        return -ret;
+    }
+
+    // Try to connect udp socket to plat_subnet(96 bits):plat_suffix(32 bits)
+    struct sockaddr_in6 dst = {
+            .sin6_family = AF_INET6,
+            .sin6_addr = *plat_subnet,
+    };
+    dst.sin6_addr.s6_addr32[3] = plat_suffix;
+    if (connect(s, (struct sockaddr*)&dst, sizeof(dst))) {
+        int ret = errno;
+        ALOGE("connect() failed: %s", strerror(errno));
+        close(s);
+        return -ret;
+    }
+
+    // Fetch the socket's IPv6 mtu - this is effectively fetching mtu from routing table
+    int mtu;
+    socklen_t sz_mtu = sizeof(mtu);
+    if (getsockopt(s, SOL_IPV6, IPV6_MTU, &mtu, &sz_mtu)) {
+        int ret = errno;
+        ALOGE("getsockopt(SOL_IPV6, IPV6_MTU) failed: %s", strerror(errno));
+        close(s);
+        return -ret;
+    }
+    if (sz_mtu != sizeof(mtu)) {
+        ALOGE("getsockopt(SOL_IPV6, IPV6_MTU) returned unexpected size: %d", sz_mtu);
+        close(s);
+        return -EFAULT;
+    }
+    close(s);
+
+    return mtu;
+}
+
+/* function: configure_packet_socket
+ * Binds the packet socket and attaches the receive filter to it.
+ *   sock    - the socket to configure
+ *   addr    - the IP address to filter
+ *   ifindex - index of interface to add the filter to
+ * returns: 0 on success, -errno on failure
+ */
+int configure_packet_socket(int sock, in6_addr* addr, int ifindex) {
+    uint32_t* ipv6 = addr->s6_addr32;
+
+    // clang-format off
+    struct sock_filter filter_code[] = {
+    // Load the first four bytes of the IPv6 destination address (starts 24 bytes in).
+    // Compare it against the first four bytes of our IPv6 address, in host byte order (BPF loads
+    // are always in host byte order). If it matches, continue with next instruction (JMP 0). If it
+    // doesn't match, jump ahead to statement that returns 0 (ignore packet). Repeat for the other
+    // three words of the IPv6 address, and if they all match, return PACKETLEN (accept packet).
+        BPF_STMT(BPF_LD  | BPF_W   | BPF_ABS,  24),
+        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,    htonl(ipv6[0]), 0, 7),
+        BPF_STMT(BPF_LD  | BPF_W   | BPF_ABS,  28),
+        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,    htonl(ipv6[1]), 0, 5),
+        BPF_STMT(BPF_LD  | BPF_W   | BPF_ABS,  32),
+        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,    htonl(ipv6[2]), 0, 3),
+        BPF_STMT(BPF_LD  | BPF_W   | BPF_ABS,  36),
+        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,    htonl(ipv6[3]), 0, 1),
+        BPF_STMT(BPF_RET | BPF_K,              PACKETLEN),
+        BPF_STMT(BPF_RET | BPF_K,              0),
+    };
+    // clang-format on
+    struct sock_fprog filter = {sizeof(filter_code) / sizeof(filter_code[0]), filter_code};
+
+    if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter))) {
+        int res = errno;
+        ALOGE("attach packet filter failed: %s", strerror(errno));
+        return -res;
+    }
+
+    struct sockaddr_ll sll = {
+            .sll_family = AF_PACKET,
+            .sll_protocol = htons(ETH_P_IPV6),
+            .sll_ifindex = ifindex,
+            .sll_pkttype =
+                    PACKET_OTHERHOST,  // The 464xlat IPv6 address is not assigned to the kernel.
+    };
+    if (bind(sock, (struct sockaddr*)&sll, sizeof(sll))) {
+        int res = errno;
+        ALOGE("binding packet socket: %s", strerror(errno));
+        return -res;
+    }
+
+    return 0;
+}
+
+}  // namespace clat
+}  // namespace net
+}  // namespace android
diff --git a/service/native/libs/libclat/clatutils_test.cpp b/service/native/libs/libclat/clatutils_test.cpp
new file mode 100644
index 0000000..4153e19
--- /dev/null
+++ b/service/native/libs/libclat/clatutils_test.cpp
@@ -0,0 +1,187 @@
+// Copyright (C) 2022 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 "libclat/clatutils.h"
+
+#include <android-base/stringprintf.h>
+#include <arpa/inet.h>
+#include <gtest/gtest.h>
+#include <linux/if_packet.h>
+#include <linux/if_tun.h>
+#include "tun_interface.h"
+
+extern "C" {
+#include "checksum.h"
+}
+
+// Default translation parameters.
+static const char kIPv4LocalAddr[] = "192.0.0.4";
+
+namespace android {
+namespace net {
+namespace clat {
+
+using android::net::TunInterface;
+using base::StringPrintf;
+
+class ClatUtils : public ::testing::Test {};
+
+// Mock functions for isIpv4AddressFree.
+bool neverFree(in_addr_t /* addr */) {
+    return 0;
+}
+bool alwaysFree(in_addr_t /* addr */) {
+    return 1;
+}
+bool only2Free(in_addr_t addr) {
+    return (ntohl(addr) & 0xff) == 2;
+}
+bool over6Free(in_addr_t addr) {
+    return (ntohl(addr) & 0xff) >= 6;
+}
+bool only10Free(in_addr_t addr) {
+    return (ntohl(addr) & 0xff) == 10;
+}
+
+// Apply mocked isIpv4AddressFree function for selectIpv4Address test.
+in_addr_t selectIpv4Address(const in_addr ip, int16_t prefixlen,
+                            isIpv4AddrFreeFn fn /* mocked function */) {
+    // Call internal function to replace isIpv4AddressFreeFn for testing.
+    return selectIpv4AddressInternal(ip, prefixlen, fn);
+}
+
+TEST_F(ClatUtils, SelectIpv4Address) {
+    struct in_addr addr;
+
+    inet_pton(AF_INET, kIPv4LocalAddr, &addr);
+
+    // If no addresses are free, return INADDR_NONE.
+    EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 29, neverFree));
+    EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 16, neverFree));
+
+    // If the configured address is free, pick that. But a prefix that's too big is invalid.
+    EXPECT_EQ(inet_addr(kIPv4LocalAddr), selectIpv4Address(addr, 29, alwaysFree));
+    EXPECT_EQ(inet_addr(kIPv4LocalAddr), selectIpv4Address(addr, 20, alwaysFree));
+    EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 15, alwaysFree));
+
+    // A prefix length of 32 works, but anything above it is invalid.
+    EXPECT_EQ(inet_addr(kIPv4LocalAddr), selectIpv4Address(addr, 32, alwaysFree));
+    EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 33, alwaysFree));
+
+    // If another address is free, pick it.
+    EXPECT_EQ(inet_addr("192.0.0.6"), selectIpv4Address(addr, 29, over6Free));
+
+    // Check that we wrap around to addresses that are lower than the first address.
+    EXPECT_EQ(inet_addr("192.0.0.2"), selectIpv4Address(addr, 29, only2Free));
+    EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 30, only2Free));
+
+    // If a free address exists outside the prefix, we don't pick it.
+    EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 29, only10Free));
+    EXPECT_EQ(inet_addr("192.0.0.10"), selectIpv4Address(addr, 24, only10Free));
+
+    // Now try using the real function which sees if IP addresses are free using bind().
+    // Assume that the machine running the test has the address 127.0.0.1, but not 8.8.8.8.
+    addr.s_addr = inet_addr("8.8.8.8");
+    EXPECT_EQ(inet_addr("8.8.8.8"), selectIpv4Address(addr, 29));
+
+    addr.s_addr = inet_addr("127.0.0.1");
+    EXPECT_EQ(inet_addr("127.0.0.2"), selectIpv4Address(addr, 29));
+}
+
+TEST_F(ClatUtils, MakeChecksumNeutral) {
+    // We can't test generateIPv6Address here since it requires manipulating routing, which we can't
+    // do without talking to the real netd on the system.
+    uint32_t rand = arc4random_uniform(0xffffffff);
+    uint16_t rand1 = rand & 0xffff;
+    uint16_t rand2 = (rand >> 16) & 0xffff;
+    std::string v6PrefixStr = StringPrintf("2001:db8:%x:%x", rand1, rand2);
+    std::string v6InterfaceAddrStr = StringPrintf("%s::%x:%x", v6PrefixStr.c_str(), rand2, rand1);
+    std::string nat64PrefixStr = StringPrintf("2001:db8:%x:%x::", rand2, rand1);
+
+    in_addr v4 = {inet_addr(kIPv4LocalAddr)};
+    in6_addr v6InterfaceAddr;
+    ASSERT_TRUE(inet_pton(AF_INET6, v6InterfaceAddrStr.c_str(), &v6InterfaceAddr));
+    in6_addr nat64Prefix;
+    ASSERT_TRUE(inet_pton(AF_INET6, nat64PrefixStr.c_str(), &nat64Prefix));
+
+    // Generate a boatload of random IIDs.
+    int onebits = 0;
+    uint64_t prev_iid = 0;
+    for (int i = 0; i < 100000; i++) {
+        in6_addr v6 = v6InterfaceAddr;
+        makeChecksumNeutral(&v6, v4, nat64Prefix);
+
+        // Check the generated IP address is in the same prefix as the interface IPv6 address.
+        EXPECT_EQ(0, memcmp(&v6, &v6InterfaceAddr, 8));
+
+        // Check that consecutive IIDs are not the same.
+        uint64_t iid = *(uint64_t*)(&v6.s6_addr[8]);
+        ASSERT_TRUE(iid != prev_iid)
+                << "Two consecutive random IIDs are the same: " << std::showbase << std::hex << iid
+                << "\n";
+        prev_iid = iid;
+
+        // Check that the IID is checksum-neutral with the NAT64 prefix and the
+        // local prefix.
+        uint16_t c1 = ip_checksum_finish(ip_checksum_add(0, &v4, sizeof(v4)));
+        uint16_t c2 = ip_checksum_finish(ip_checksum_add(0, &nat64Prefix, sizeof(nat64Prefix)) +
+                                         ip_checksum_add(0, &v6, sizeof(v6)));
+
+        if (c1 != c2) {
+            char v6Str[INET6_ADDRSTRLEN];
+            inet_ntop(AF_INET6, &v6, v6Str, sizeof(v6Str));
+            FAIL() << "Bad IID: " << v6Str << " not checksum-neutral with " << kIPv4LocalAddr
+                   << " and " << nat64PrefixStr.c_str() << std::showbase << std::hex
+                   << "\n  IPv4 checksum: " << c1 << "\n  IPv6 checksum: " << c2 << "\n";
+        }
+
+        // Check that IIDs are roughly random and use all the bits by counting the
+        // total number of bits set to 1 in a random sample of 100000 generated IIDs.
+        onebits += __builtin_popcountll(*(uint64_t*)&iid);
+    }
+    EXPECT_LE(3190000, onebits);
+    EXPECT_GE(3210000, onebits);
+}
+
+TEST_F(ClatUtils, DetectMtu) {
+    // ::1 with bottom 32 bits set to 1 is still ::1 which routes via lo with mtu of 64KiB
+    ASSERT_EQ(detect_mtu(&in6addr_loopback, htonl(1), 0 /*MARK_UNSET*/), 65536);
+}
+
+TEST_F(ClatUtils, ConfigurePacketSocket) {
+    // Create an interface for configure_packet_socket to attach socket filter to.
+    TunInterface v6Iface;
+    ASSERT_EQ(0, v6Iface.init());
+
+    int s = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC, htons(ETH_P_IPV6));
+    EXPECT_LE(0, s);
+    struct in6_addr addr6;
+    EXPECT_EQ(1, inet_pton(AF_INET6, "2001:db8::f00", &addr6));
+    EXPECT_EQ(0, configure_packet_socket(s, &addr6, v6Iface.ifindex()));
+
+    // Check that the packet socket is bound to the interface. We can't check the socket filter
+    // because there is no way to fetch it from the kernel.
+    sockaddr_ll sll;
+    socklen_t len = sizeof(sll);
+    ASSERT_EQ(0, getsockname(s, reinterpret_cast<sockaddr*>(&sll), &len));
+    EXPECT_EQ(htons(ETH_P_IPV6), sll.sll_protocol);
+    EXPECT_EQ(sll.sll_ifindex, v6Iface.ifindex());
+
+    close(s);
+    v6Iface.destroy();
+}
+
+}  // namespace clat
+}  // namespace net
+}  // namespace android
diff --git a/service/native/libs/libclat/include/libclat/TcUtils.h b/service/native/libs/libclat/include/libclat/TcUtils.h
new file mode 100644
index 0000000..212838e
--- /dev/null
+++ b/service/native/libs/libclat/include/libclat/TcUtils.h
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+#pragma once
+
+#include <android-base/result.h>
+#include <errno.h>
+#include <linux/if_ether.h>
+#include <linux/if_link.h>
+#include <linux/rtnetlink.h>
+
+#include <string>
+
+#include "bpf/BpfUtils.h"
+#include "bpf_shared.h"
+
+namespace android {
+namespace net {
+
+// For better code clarity - do not change values - used for booleans like
+// with_ethernet_header or isEthernet.
+constexpr bool RAWIP = false;
+constexpr bool ETHER = true;
+
+// For better code clarity when used for 'bool ingress' parameter.
+constexpr bool EGRESS = false;
+constexpr bool INGRESS = true;
+
+// The priority of clat hook - must be after tethering.
+constexpr uint16_t PRIO_CLAT = 4;
+
+// this returns an ARPHRD_* constant or a -errno
+int hardwareAddressType(const std::string& interface);
+
+// return MTU or -errno
+int deviceMTU(const std::string& interface);
+
+base::Result<bool> isEthernet(const std::string& interface);
+
+inline int getClatEgress4MapFd(void) {
+    const int fd = bpf::mapRetrieveRW(CLAT_EGRESS4_MAP_PATH);
+    return (fd == -1) ? -errno : fd;
+}
+
+inline int getClatEgress4ProgFd(bool with_ethernet_header) {
+    const int fd = bpf::retrieveProgram(with_ethernet_header ? CLAT_EGRESS4_PROG_ETHER_PATH
+                                                             : CLAT_EGRESS4_PROG_RAWIP_PATH);
+    return (fd == -1) ? -errno : fd;
+}
+
+inline int getClatIngress6MapFd(void) {
+    const int fd = bpf::mapRetrieveRW(CLAT_INGRESS6_MAP_PATH);
+    return (fd == -1) ? -errno : fd;
+}
+
+inline int getClatIngress6ProgFd(bool with_ethernet_header) {
+    const int fd = bpf::retrieveProgram(with_ethernet_header ? CLAT_INGRESS6_PROG_ETHER_PATH
+                                                             : CLAT_INGRESS6_PROG_RAWIP_PATH);
+    return (fd == -1) ? -errno : fd;
+}
+
+int doTcQdiscClsact(int ifIndex, uint16_t nlMsgType, uint16_t nlMsgFlags);
+
+inline int tcQdiscAddDevClsact(int ifIndex) {
+    return doTcQdiscClsact(ifIndex, RTM_NEWQDISC, NLM_F_EXCL | NLM_F_CREATE);
+}
+
+inline int tcQdiscReplaceDevClsact(int ifIndex) {
+    return doTcQdiscClsact(ifIndex, RTM_NEWQDISC, NLM_F_CREATE | NLM_F_REPLACE);
+}
+
+inline int tcQdiscDelDevClsact(int ifIndex) {
+    return doTcQdiscClsact(ifIndex, RTM_DELQDISC, 0);
+}
+
+// tc filter add dev .. in/egress prio 4 protocol ipv6/ip bpf object-pinned /sys/fs/bpf/...
+// direct-action
+int tcFilterAddDevBpf(int ifIndex, bool ingress, uint16_t proto, int bpfFd, bool ethernet);
+
+// tc filter add dev .. ingress prio 4 protocol ipv6 bpf object-pinned /sys/fs/bpf/... direct-action
+inline int tcFilterAddDevIngressClatIpv6(int ifIndex, int bpfFd, bool ethernet) {
+    return tcFilterAddDevBpf(ifIndex, INGRESS, ETH_P_IPV6, bpfFd, ethernet);
+}
+
+// tc filter add dev .. egress prio 4 protocol ip bpf object-pinned /sys/fs/bpf/... direct-action
+inline int tcFilterAddDevEgressClatIpv4(int ifIndex, int bpfFd, bool ethernet) {
+    return tcFilterAddDevBpf(ifIndex, EGRESS, ETH_P_IP, bpfFd, ethernet);
+}
+
+// tc filter del dev .. in/egress prio .. protocol ..
+int tcFilterDelDev(int ifIndex, bool ingress, uint16_t prio, uint16_t proto);
+
+// tc filter del dev .. ingress prio 4 protocol ipv6
+inline int tcFilterDelDevIngressClatIpv6(int ifIndex) {
+    return tcFilterDelDev(ifIndex, INGRESS, PRIO_CLAT, ETH_P_IPV6);
+}
+
+// tc filter del dev .. egress prio 4 protocol ip
+inline int tcFilterDelDevEgressClatIpv4(int ifIndex) {
+    return tcFilterDelDev(ifIndex, EGRESS, PRIO_CLAT, ETH_P_IP);
+}
+
+}  // namespace net
+}  // namespace android
diff --git a/service/native/libs/libclat/include/libclat/bpfhelper.h b/service/native/libs/libclat/include/libclat/bpfhelper.h
new file mode 100644
index 0000000..c0328c0
--- /dev/null
+++ b/service/native/libs/libclat/include/libclat/bpfhelper.h
@@ -0,0 +1,40 @@
+// 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.
+
+#pragma once
+
+#include <arpa/inet.h>
+#include <linux/if.h>
+
+namespace android {
+namespace net {
+namespace clat {
+
+struct ClatdTracker {
+    unsigned ifIndex;
+    char iface[IFNAMSIZ];
+    unsigned v4ifIndex;
+    char v4iface[IFNAMSIZ];
+    in_addr v4;
+    in6_addr v6;
+    in6_addr pfx96;
+};
+
+int initMaps(void);
+void maybeStartBpf(const ClatdTracker& tracker);
+void maybeStopBpf(const ClatdTracker& tracker);
+
+}  // namespace clat
+}  // namespace net
+}  // namespace android
diff --git a/service/native/libs/libclat/include/libclat/clatutils.h b/service/native/libs/libclat/include/libclat/clatutils.h
new file mode 100644
index 0000000..812c86e
--- /dev/null
+++ b/service/native/libs/libclat/include/libclat/clatutils.h
@@ -0,0 +1,37 @@
+// Copyright (C) 2022 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.
+
+#pragma once
+#include <netinet/in.h>
+#include <netinet/in6.h>
+
+namespace android {
+namespace net {
+namespace clat {
+
+bool isIpv4AddressFree(in_addr_t addr);
+in_addr_t selectIpv4Address(const in_addr ip, int16_t prefixlen);
+void makeChecksumNeutral(in6_addr* v6, const in_addr v4, const in6_addr& nat64Prefix);
+int generateIpv6Address(const char* iface, const in_addr v4, const in6_addr& nat64Prefix,
+                        in6_addr* v6);
+int detect_mtu(const struct in6_addr* plat_subnet, uint32_t plat_suffix, uint32_t mark);
+int configure_packet_socket(int sock, in6_addr* addr, int ifindex);
+
+// For testing
+typedef bool (*isIpv4AddrFreeFn)(in_addr_t);
+in_addr_t selectIpv4AddressInternal(const in_addr ip, int16_t prefixlen, isIpv4AddrFreeFn fn);
+
+}  // namespace clat
+}  // namespace net
+}  // namespace android
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
new file mode 100644
index 0000000..9d89788
--- /dev/null
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2022 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.server;
+
+import android.os.ServiceSpecificException;
+import android.system.Os;
+import android.util.Log;
+
+/**
+ * BpfNetMaps is responsible for providing traffic controller relevant functionality.
+ *
+ * {@hide}
+ */
+public class BpfNetMaps {
+    private static final String TAG = "BpfNetMaps";
+
+    static {
+        System.loadLibrary("traffic_controller_jni");
+        native_init();
+    }
+
+   /**
+    * Add naughty app bandwidth rule for specific app
+    *
+    * @param uid uid of target app
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    public void addNaughtyApp(final int uid) {
+        final int err = native_addNaughtyApp(uid);
+        if (err != 0) {
+            throw new ServiceSpecificException(err, "Unable to add naughty app: "
+                            + Os.strerror(err));
+        }
+    }
+
+   /**
+    * Remove naughty app bandwidth rule for specific app
+    *
+    * @param uid uid of target app
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    public void removeNaughtyApp(final int uid) {
+        final int err = native_removeNaughtyApp(uid);
+        if (err != 0) {
+            throw new ServiceSpecificException(err, "Unable to remove naughty app: "
+                            + Os.strerror(err));
+        }
+    }
+
+   /**
+    * Add nice app bandwidth rule for specific app
+    *
+    * @param uid uid of target app
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    public void addNiceApp(final int uid) {
+        final int err = native_addNiceApp(uid);
+        if (err != 0) {
+            throw new ServiceSpecificException(err, "Unable to add nice app: "
+                            + Os.strerror(err));
+        }
+    }
+
+   /**
+    * Remove nice app bandwidth rule for specific app
+    *
+    * @param uid uid of target app
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    public void removeNiceApp(final int uid) {
+        final int err = native_removeNiceApp(uid);
+        if (err != 0) {
+            throw new ServiceSpecificException(err, "Unable to remove nice app: "
+                            + Os.strerror(err));
+        }
+    }
+
+   /**
+    * Set target firewall child chain
+    *
+    * @param childChain target chain to enable
+    * @param enable whether to enable or disable child chain.
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    public void setChildChain(final int childChain, final boolean enable) {
+        final int err = native_setChildChain(childChain, enable);
+        if (err != 0) {
+            throw new ServiceSpecificException(-err, "Unable to set child chain: "
+                            + Os.strerror(-err));
+        }
+    }
+
+    /**
+     * Replaces the contents of the specified UID-based firewall chain.
+     *
+     * The chain may be an allowlist chain or a denylist chain. A denylist chain contains DROP
+     * rules for the specified UIDs and a RETURN rule at the end. An allowlist chain contains RETURN
+     * rules for the system UID range (0 to {@code UID_APP} - 1), RETURN rules for for the specified
+     * UIDs, and a DROP rule at the end. The chain will be created if it does not exist.
+     *
+     * @param chainName The name of the chain to replace.
+     * @param isAllowlist Whether this is an allowlist or denylist chain.
+     * @param uids The list of UIDs to allow/deny.
+     * @return true if the chain was successfully replaced, false otherwise.
+     */
+    public int replaceUidChain(final String chainName, final boolean isAllowlist,
+            final int[] uids) {
+        final int err = native_replaceUidChain(chainName, isAllowlist, uids);
+        if (err != 0) {
+            Log.e(TAG, "replaceUidChain failed: " + Os.strerror(-err));
+        }
+        return -err;
+    }
+
+   /**
+    * Set firewall rule for uid
+    *
+    * @param childChain target chain
+    * @param uid uid to allow/deny
+    * @param firewallRule either FIREWALL_RULE_ALLOW or FIREWALL_RULE_DENY
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    public void setUidRule(final int childChain, final int uid,
+            final int firewallRule) {
+        final int err = native_setUidRule(childChain, uid, firewallRule);
+        if (err != 0) {
+            throw new ServiceSpecificException(-err, "Unable to set uid rule: "
+                            + Os.strerror(-err));
+        }
+    }
+
+    /**
+     * Add ingress interface filtering rules to a list of UIDs
+     *
+     * For a given uid, once a filtering rule is added, the kernel will only allow packets from the
+     * allowed interface and loopback to be sent to the list of UIDs.
+     *
+     * Calling this method on one or more UIDs with an existing filtering rule but a different
+     * interface name will result in the filtering rule being updated to allow the new interface
+     * instead. Otherwise calling this method will not affect existing rules set on other UIDs.
+     *
+     * @param ifName the name of the interface on which the filtering rules will allow packets to
+              be received.
+     * @param uids an array of UIDs which the filtering rules will be set
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *         cause of the failure.
+     */
+    public void addUidInterfaceRules(final String ifName, final int[] uids) {
+        final int err = native_addUidInterfaceRules(ifName, uids);
+        if (err != 0) {
+            throw new ServiceSpecificException(err, "Unable to add uid interface rules: "
+                            + Os.strerror(err));
+        }
+    }
+
+    /**
+     * Remove ingress interface filtering rules from a list of UIDs
+     *
+     * Clear the ingress interface filtering rules from the list of UIDs which were previously set
+     * by addUidInterfaceRules(). Ignore any uid which does not have filtering rule.
+     *
+     * @param uids an array of UIDs from which the filtering rules will be removed
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *         cause of the failure.
+     */
+    public void removeUidInterfaceRules(final int[] uids) {
+        final int err = native_removeUidInterfaceRules(uids);
+        if (err != 0) {
+            throw new ServiceSpecificException(err, "Unable to remove uid interface rules: "
+                            + Os.strerror(err));
+        }
+    }
+
+   /**
+    * Request netd to change the current active network stats map.
+    * @throws ServiceSpecificException in case of failure, with an error code indicating the
+    *         cause of the failure.
+    */
+    public void swapActiveStatsMap() {
+        final int err = native_swapActiveStatsMap();
+        if (err != 0) {
+            throw new ServiceSpecificException(err, "Unable to swap active stats map: "
+                            + Os.strerror(err));
+        }
+    }
+
+   /**
+    * Assigns android.permission.INTERNET and/or android.permission.UPDATE_DEVICE_STATS to the uids
+    * specified. Or remove all permissions from the uids.
+    *
+    * @param permission The permission to grant, it could be either PERMISSION_INTERNET and/or
+    *                   PERMISSION_UPDATE_DEVICE_STATS. If the permission is NO_PERMISSIONS, then
+    *                   revoke all permissions for the uids.
+    * @param uids uid of users to grant permission
+    */
+    public void setNetPermForUids(final int permission, final int[] uids) {
+        native_setPermissionForUids(permission, uids);
+    }
+
+    /**
+     * Set counter set for uid
+     *
+     * @param counterSet either SET_DEFAULT or SET_FOREGROUND
+     * @param uid uid to foreground/background
+     */
+    public int setCounterSet(final int counterSet, final int uid) {
+        final int err = native_setCounterSet(counterSet, uid);
+        if (err != 0) {
+            Log.e(TAG, "setCounterSet failed: " + Os.strerror(-err));
+        }
+        return -err;
+    }
+
+    /**
+     * Reset Uid stats
+     * @param tag default 0
+     * @param uid given uid to be clear
+     */
+    public int deleteTagData(final int tag, final int uid) {
+        final int err = native_deleteTagData(tag, uid);
+        if (err != 0) {
+            Log.e(TAG, "deleteTagData failed: " + Os.strerror(-err));
+        }
+        return -err;
+    }
+
+    private static native void native_init();
+    private native int native_addNaughtyApp(int uid);
+    private native int native_removeNaughtyApp(int uid);
+    private native int native_addNiceApp(int uid);
+    private native int native_removeNiceApp(int uid);
+    private native int native_setChildChain(int childChain, boolean enable);
+    private native int native_replaceUidChain(String name, boolean isAllowlist, int[] uids);
+    private native int native_setUidRule(int childChain, int uid, int firewallRule);
+    private native int native_addUidInterfaceRules(String ifName, int[] uids);
+    private native int native_removeUidInterfaceRules(int[] uids);
+    private native int native_swapActiveStatsMap();
+    private native void native_setPermissionForUids(int permission, int[] uids);
+    private native int native_setCounterSet(int counterSet, int uid);
+    private native int native_deleteTagData(int tag, int uid);
+}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index d625d1b..fa7dfa9 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -73,6 +73,8 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5;
 import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION;
 import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
 import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
@@ -124,6 +126,7 @@
 import android.net.ConnectivitySettingsManager;
 import android.net.DataStallReportParcelable;
 import android.net.DnsResolverServiceManager;
+import android.net.DscpPolicy;
 import android.net.ICaptivePortal;
 import android.net.IConnectivityDiagnosticsCallback;
 import android.net.IConnectivityManager;
@@ -218,6 +221,7 @@
 import android.os.UserManager;
 import android.provider.Settings;
 import android.sysprop.NetworkProperties;
+import android.system.ErrnoException;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.ArrayMap;
@@ -248,6 +252,7 @@
 import com.android.server.connectivity.ConnectivityFlags;
 import com.android.server.connectivity.DnsManager;
 import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
+import com.android.server.connectivity.DscpPolicyTracker;
 import com.android.server.connectivity.FullScore;
 import com.android.server.connectivity.KeepaliveTracker;
 import com.android.server.connectivity.LingerMonitor;
@@ -262,6 +267,7 @@
 import com.android.server.connectivity.ProfileNetworkPreferenceList;
 import com.android.server.connectivity.ProxyTracker;
 import com.android.server.connectivity.QosCallbackTracker;
+import com.android.server.connectivity.UidRangeUtils;
 
 import libcore.io.IoUtils;
 
@@ -386,6 +392,7 @@
     protected IDnsResolver mDnsResolver;
     @VisibleForTesting
     protected INetd mNetd;
+    private DscpPolicyTracker mDscpPolicyTracker = null;
     private NetworkStatsManager mStatsManager;
     private NetworkPolicyManager mPolicyManager;
     private final NetdCallback mNetdCallback;
@@ -1486,19 +1493,33 @@
                 new NetworkScore.Builder().setLegacyInt(0).build(), mContext, null,
                 new NetworkAgentConfig(), this, null, null, 0, INVALID_UID,
                 mLingerDelayMs, mQosCallbackTracker, mDeps);
+
+        try {
+            // DscpPolicyTracker cannot run on S because on S the tethering module can only load
+            // BPF programs/maps into /sys/fs/tethering/bpf, which the system server cannot access.
+            // Even if it could, running on S would at least require mocking out the BPF map,
+            // otherwise the unit tests will fail on pre-T devices where the seccomp filter blocks
+            // the bpf syscall. http://aosp/1907693
+            if (SdkLevel.isAtLeastT()) {
+                mDscpPolicyTracker = new DscpPolicyTracker();
+            }
+        } catch (ErrnoException e) {
+            loge("Unable to create DscpPolicyTracker");
+        }
     }
 
     private static NetworkCapabilities createDefaultNetworkCapabilitiesForUid(int uid) {
-        return createDefaultNetworkCapabilitiesForUidRange(new UidRange(uid, uid));
+        return createDefaultNetworkCapabilitiesForUidRangeSet(Collections.singleton(
+                new UidRange(uid, uid)));
     }
 
-    private static NetworkCapabilities createDefaultNetworkCapabilitiesForUidRange(
-            @NonNull final UidRange uids) {
+    private static NetworkCapabilities createDefaultNetworkCapabilitiesForUidRangeSet(
+            @NonNull final Set<UidRange> uidRangeSet) {
         final NetworkCapabilities netCap = new NetworkCapabilities();
         netCap.addCapability(NET_CAPABILITY_INTERNET);
         netCap.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
         netCap.removeCapability(NET_CAPABILITY_NOT_VPN);
-        netCap.setUids(UidRange.toIntRanges(Collections.singleton(uids)));
+        netCap.setUids(UidRange.toIntRanges(uidRangeSet));
         return netCap;
     }
 
@@ -3402,6 +3423,25 @@
                     nai.setLingerDuration((int) arg.second);
                     break;
                 }
+                case NetworkAgent.EVENT_ADD_DSCP_POLICY: {
+                    DscpPolicy policy = (DscpPolicy) arg.second;
+                    if (mDscpPolicyTracker != null) {
+                        mDscpPolicyTracker.addDscpPolicy(nai, policy);
+                    }
+                    break;
+                }
+                case NetworkAgent.EVENT_REMOVE_DSCP_POLICY: {
+                    if (mDscpPolicyTracker != null) {
+                        mDscpPolicyTracker.removeDscpPolicy(nai, (int) arg.second);
+                    }
+                    break;
+                }
+                case NetworkAgent.EVENT_REMOVE_ALL_DSCP_POLICIES: {
+                    if (mDscpPolicyTracker != null) {
+                        mDscpPolicyTracker.removeAllDscpPolicies(nai);
+                    }
+                    break;
+                }
             }
         }
 
@@ -4042,7 +4082,7 @@
                 config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.VIRTUAL,
                         INetd.PERMISSION_NONE,
                         (nai.networkAgentConfig == null || !nai.networkAgentConfig.allowBypass),
-                        getVpnType(nai), /*excludeLocalRoutes=*/ false);
+                        getVpnType(nai), nai.networkAgentConfig.excludeLocalRouteVpn);
             } else {
                 config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.PHYSICAL,
                         getNetworkPermission(nai.networkCapabilities), /*secure=*/ false,
@@ -10145,14 +10185,30 @@
             switch (preference.getPreference()) {
                 case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT:
                     nc = null;
+                    if (preference.getPreferenceEnterpriseId() != 0) {
+                        throw new IllegalArgumentException(
+                                "Invalid enterprise identifier in setProfileNetworkPreferences");
+                    }
                     break;
                 case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK:
                     allowFallback = false;
                     // continue to process the enterprise preference.
                 case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE:
-                    final UidRange uids = UidRange.createForUser(profile);
-                    nc = createDefaultNetworkCapabilitiesForUidRange(uids);
+                    if (!isEnterpriseIdentifierValid(preference.getPreferenceEnterpriseId())) {
+                        throw new IllegalArgumentException(
+                                "Invalid enterprise identifier in setProfileNetworkPreferences");
+                    }
+                    final Set<UidRange> uidRangeSet =
+                            getUidListToBeAppliedForNetworkPreference(profile, preference);
+                    if (!isRangeAlreadyInPreferenceList(preferenceList, uidRangeSet)) {
+                        nc = createDefaultNetworkCapabilitiesForUidRangeSet(uidRangeSet);
+                    } else {
+                        throw new IllegalArgumentException(
+                                "Overlapping uid range in setProfileNetworkPreferences");
+                    }
                     nc.addCapability(NET_CAPABILITY_ENTERPRISE);
+                    nc.addEnterpriseId(
+                            preference.getPreferenceEnterpriseId());
                     nc.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
                     break;
                 default:
@@ -10166,6 +10222,44 @@
                 new Pair<>(preferenceList, listener)));
     }
 
+    private Set<UidRange> getUidListToBeAppliedForNetworkPreference(
+            @NonNull final UserHandle profile,
+            @NonNull final ProfileNetworkPreference profileNetworkPreference) {
+        final UidRange profileUids = UidRange.createForUser(profile);
+        Set<UidRange> uidRangeSet = UidRangeUtils.convertListToUidRange(
+                profileNetworkPreference.getIncludedUids());
+        if (uidRangeSet.size() > 0) {
+            if (!UidRangeUtils.isRangeSetInUidRange(profileUids, uidRangeSet)) {
+                throw new IllegalArgumentException(
+                        "Allow uid range is outside the uid range of profile.");
+            }
+        } else {
+            ArraySet<UidRange> disallowUidRangeSet = UidRangeUtils.convertListToUidRange(
+                    profileNetworkPreference.getExcludedUids());
+            if (disallowUidRangeSet.size() > 0) {
+                if (!UidRangeUtils.isRangeSetInUidRange(profileUids, disallowUidRangeSet)) {
+                    throw new IllegalArgumentException(
+                            "disallow uid range is outside the uid range of profile.");
+                }
+                uidRangeSet = UidRangeUtils.removeRangeSetFromUidRange(profileUids,
+                        disallowUidRangeSet);
+            } else {
+                uidRangeSet = new ArraySet<UidRange>();
+                uidRangeSet.add(profileUids);
+            }
+        }
+        return uidRangeSet;
+    }
+
+    private boolean isEnterpriseIdentifierValid(
+            @NetworkCapabilities.EnterpriseId int identifier) {
+        if ((identifier >= NET_ENTERPRISE_ID_1)
+                && (identifier <= NET_ENTERPRISE_ID_5)) {
+            return true;
+        }
+        return false;
+    }
+
     private void validateNetworkCapabilitiesOfProfileNetworkPreference(
             @Nullable final NetworkCapabilities nc) {
         if (null == nc) return; // Null caps are always allowed. It means to remove the setting.
@@ -10187,6 +10281,11 @@
                 nrs.add(createDefaultInternetRequestForTransport(
                         TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT));
             }
+            if (VDBG) {
+                loge("pref.capabilities.getUids():" + UidRange.fromIntRanges(
+                        pref.capabilities.getUids()));
+            }
+
             setNetworkRequestUids(nrs, UidRange.fromIntRanges(pref.capabilities.getUids()));
             final NetworkRequestInfo nri = new NetworkRequestInfo(Process.myUid(), nrs,
                     PREFERENCE_ORDER_PROFILE);
@@ -10195,6 +10294,25 @@
         return result;
     }
 
+    /**
+     * Compare if the given UID range sets have the same UIDs.
+     *
+     */
+    private boolean isRangeAlreadyInPreferenceList(
+            @NonNull List<ProfileNetworkPreferenceList.Preference> preferenceList,
+            @NonNull Set<UidRange> uidRangeSet) {
+        if (uidRangeSet.size() == 0 || preferenceList.size() == 0) {
+            return false;
+        }
+        for (ProfileNetworkPreferenceList.Preference pref : preferenceList) {
+            if (UidRangeUtils.doesRangeSetOverlap(
+                    UidRange.fromIntRanges(pref.capabilities.getUids()), uidRangeSet)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private void handleSetProfileNetworkPreference(
             @NonNull final List<ProfileNetworkPreferenceList.Preference> preferenceList,
             @Nullable final IOnCompleteListener listener) {
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
new file mode 100644
index 0000000..c57983b
--- /dev/null
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright (C) 2022 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.server.connectivity;
+
+import static android.net.INetd.IF_STATE_UP;
+import static android.net.INetd.PERMISSION_SYSTEM;
+
+import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.INetd;
+import android.net.InterfaceConfigurationParcel;
+import android.net.IpPrefix;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.InterfaceParams;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+
+/**
+ * This coordinator is responsible for providing clat relevant functionality.
+ *
+ * {@hide}
+ */
+public class ClatCoordinator {
+    private static final String TAG = ClatCoordinator.class.getSimpleName();
+
+    // Sync from external/android-clat/clatd.c
+    // 40 bytes IPv6 header - 20 bytes IPv4 header + 8 bytes fragment header.
+    @VisibleForTesting
+    static final int MTU_DELTA = 28;
+    @VisibleForTesting
+    static final int CLAT_MAX_MTU = 65536;
+
+    // This must match the interface prefix in clatd.c.
+    private static final String CLAT_PREFIX = "v4-";
+
+    // For historical reasons, start with 192.0.0.4, and after that, use all subsequent addresses
+    // in 192.0.0.0/29 (RFC 7335).
+    @VisibleForTesting
+    static final String INIT_V4ADDR_STRING = "192.0.0.4";
+    @VisibleForTesting
+    static final int INIT_V4ADDR_PREFIX_LEN = 29;
+    private static final InetAddress GOOGLE_DNS_4 = InetAddress.parseNumericAddress("8.8.8.8");
+
+    private static final int INVALID_IFINDEX = 0;
+    private static final int INVALID_PID = 0;
+
+    @NonNull
+    private final INetd mNetd;
+    @NonNull
+    private final Dependencies mDeps;
+    @Nullable
+    private String mIface = null;
+    @Nullable
+    private String mNat64Prefix = null;
+    @Nullable
+    private String mXlatLocalAddress4 = null;
+    @Nullable
+    private String mXlatLocalAddress6 = null;
+    private int mPid = INVALID_PID;
+
+    @VisibleForTesting
+    abstract static class Dependencies {
+        /**
+          * Get netd.
+          */
+        @NonNull
+        public abstract INetd getNetd();
+
+        /**
+         * @see ParcelFileDescriptor#adoptFd(int).
+         */
+        @NonNull
+        public ParcelFileDescriptor adoptFd(int fd) {
+            return ParcelFileDescriptor.adoptFd(fd);
+        }
+
+        /**
+         * Get interface index for a given interface.
+         */
+        public int getInterfaceIndex(String ifName) {
+            final InterfaceParams params = InterfaceParams.getByName(ifName);
+            return params != null ? params.index : INVALID_IFINDEX;
+        }
+
+        /**
+         * Create tun interface for a given interface name.
+         */
+        public int createTunInterface(@NonNull String tuniface) throws IOException {
+            return native_createTunInterface(tuniface);
+        }
+
+        /**
+         * Pick an IPv4 address for clat.
+         */
+        @NonNull
+        public String selectIpv4Address(@NonNull String v4addr, int prefixlen)
+                throws IOException {
+            return native_selectIpv4Address(v4addr, prefixlen);
+        }
+
+        /**
+         * Generate a checksum-neutral IID.
+         */
+        @NonNull
+        public String generateIpv6Address(@NonNull String iface, @NonNull String v4,
+                @NonNull String prefix64) throws IOException {
+            return native_generateIpv6Address(iface, v4, prefix64);
+        }
+
+        /**
+         * Detect MTU.
+         */
+        public int detectMtu(@NonNull String platSubnet, int platSuffix, int mark)
+                throws IOException {
+            return native_detectMtu(platSubnet, platSuffix, mark);
+        }
+
+        /**
+         * Open packet socket.
+         */
+        public int openPacketSocket() throws IOException {
+            return native_openPacketSocket();
+        }
+
+        /**
+         * Open IPv6 raw socket and set SO_MARK.
+         */
+        public int openRawSocket6(int mark) throws IOException {
+            return native_openRawSocket6(mark);
+        }
+
+        /**
+         * Add anycast setsockopt.
+         */
+        public void addAnycastSetsockopt(@NonNull FileDescriptor sock, String v6, int ifindex)
+                throws IOException {
+            native_addAnycastSetsockopt(sock, v6, ifindex);
+        }
+
+        /**
+         * Configure packet socket.
+         */
+        public void configurePacketSocket(@NonNull FileDescriptor sock, String v6, int ifindex)
+                throws IOException {
+            native_configurePacketSocket(sock, v6, ifindex);
+        }
+
+        /**
+         * Start clatd.
+         */
+        public int startClatd(@NonNull FileDescriptor tunfd, @NonNull FileDescriptor readsock6,
+                @NonNull FileDescriptor writesock6, @NonNull String iface, @NonNull String pfx96,
+                @NonNull String v4, @NonNull String v6) throws IOException {
+            return native_startClatd(tunfd, readsock6, writesock6, iface, pfx96, v4, v6);
+        }
+
+        /**
+         * Stop clatd.
+         */
+        public void stopClatd(String iface, String pfx96, String v4, String v6, int pid)
+                throws IOException {
+            native_stopClatd(iface, pfx96, v4, v6, pid);
+        }
+    }
+
+    @VisibleForTesting
+    static int getFwmark(int netId) {
+        // See union Fwmark in system/netd/include/Fwmark.h
+        return (netId & 0xffff)
+                | 0x1 << 16  // protectedFromVpn: true
+                | 0x1 << 17  // explicitlySelected: true
+                | (PERMISSION_SYSTEM & 0x3) << 18;
+    }
+
+    @VisibleForTesting
+    static int adjustMtu(int mtu) {
+        // clamp to minimum ipv6 mtu - this probably cannot ever trigger
+        if (mtu < IPV6_MIN_MTU) mtu = IPV6_MIN_MTU;
+        // clamp to buffer size
+        if (mtu > CLAT_MAX_MTU) mtu = CLAT_MAX_MTU;
+        // decrease by ipv6(40) + ipv6 fragmentation header(8) vs ipv4(20) overhead of 28 bytes
+        mtu -= MTU_DELTA;
+
+        return mtu;
+    }
+
+    public ClatCoordinator(@NonNull Dependencies deps) {
+        mDeps = deps;
+        mNetd = mDeps.getNetd();
+    }
+
+    /**
+     * Start clatd for a given interface and NAT64 prefix.
+     */
+    public String clatStart(final String iface, final int netId,
+            @NonNull final IpPrefix nat64Prefix)
+            throws IOException {
+        if (mIface != null || mPid != INVALID_PID) {
+            throw new IOException("Clatd is already running on " + mIface + " (pid " + mPid + ")");
+        }
+        if (nat64Prefix.getPrefixLength() != 96) {
+            throw new IOException("Prefix must be 96 bits long: " + nat64Prefix);
+        }
+
+        // [1] Pick an IPv4 address from 192.0.0.4, 192.0.0.5, 192.0.0.6 ..
+        final String v4;
+        try {
+            v4 = mDeps.selectIpv4Address(INIT_V4ADDR_STRING, INIT_V4ADDR_PREFIX_LEN);
+        } catch (IOException e) {
+            throw new IOException("no IPv4 addresses were available for clat: " + e);
+        }
+
+        // [2] Generate a checksum-neutral IID.
+        final String pfx96 = nat64Prefix.getAddress().getHostAddress();
+        final String v6;
+        try {
+            v6 = mDeps.generateIpv6Address(iface, v4, pfx96);
+        } catch (IOException e) {
+            throw new IOException("no IPv6 addresses were available for clat: " + e);
+        }
+
+        // [3] Open, configure and bring up the tun interface.
+        // Create the v4-... tun interface.
+        final String tunIface = CLAT_PREFIX + iface;
+        final ParcelFileDescriptor tunFd;
+        try {
+            tunFd = mDeps.adoptFd(mDeps.createTunInterface(tunIface));
+        } catch (IOException e) {
+            throw new IOException("Create tun interface " + tunIface + " failed: " + e);
+        }
+
+        // disable IPv6 on it - failing to do so is not a critical error
+        try {
+            mNetd.interfaceSetEnableIPv6(tunIface, false /* enabled */);
+        } catch (RemoteException | ServiceSpecificException e) {
+            tunFd.close();
+            Log.e(TAG, "Disable IPv6 on " + tunIface + " failed: " + e);
+        }
+
+        // Detect ipv4 mtu.
+        final Integer fwmark = getFwmark(netId);
+        final int detectedMtu = mDeps.detectMtu(pfx96,
+                ByteBuffer.wrap(GOOGLE_DNS_4.getAddress()).getInt(), fwmark);
+        final int mtu = adjustMtu(detectedMtu);
+        Log.i(TAG, "ipv4 mtu is " + mtu);
+
+        // TODO: add setIptablesDropRule
+
+        // Config tun interface mtu, address and bring up.
+        try {
+            mNetd.interfaceSetMtu(tunIface, mtu);
+        } catch (RemoteException | ServiceSpecificException e) {
+            tunFd.close();
+            throw new IOException("Set MTU " + mtu + " on " + tunIface + " failed: " + e);
+        }
+        final InterfaceConfigurationParcel ifConfig = new InterfaceConfigurationParcel();
+        ifConfig.ifName = tunIface;
+        ifConfig.ipv4Addr = v4;
+        ifConfig.prefixLength = 32;
+        ifConfig.hwAddr = "";
+        ifConfig.flags = new String[] {IF_STATE_UP};
+        try {
+            mNetd.interfaceSetCfg(ifConfig);
+        } catch (RemoteException | ServiceSpecificException e) {
+            tunFd.close();
+            throw new IOException("Setting IPv4 address to " + ifConfig.ipv4Addr + "/"
+                    + ifConfig.prefixLength + " failed on " + ifConfig.ifName + ": " + e);
+        }
+
+        // [4] Open and configure local 464xlat read/write sockets.
+        // Opens a packet socket to receive IPv6 packets in clatd.
+        final ParcelFileDescriptor readSock6;
+        try {
+            // Use a JNI call to get native file descriptor instead of Os.socket() because we would
+            // like to use ParcelFileDescriptor to manage file descriptor. But ctor
+            // ParcelFileDescriptor(FileDescriptor fd) is a @hide function. Need to use native file
+            // descriptor to initialize ParcelFileDescriptor object instead.
+            readSock6 = mDeps.adoptFd(mDeps.openPacketSocket());
+        } catch (IOException e) {
+            tunFd.close();
+            throw new IOException("Open packet socket failed: " + e);
+        }
+
+        // Opens a raw socket with a given fwmark to send IPv6 packets in clatd.
+        final ParcelFileDescriptor writeSock6;
+        try {
+            // Use a JNI call to get native file descriptor instead of Os.socket(). See above
+            // reason why we use jniOpenPacketSocket6().
+            writeSock6 = mDeps.adoptFd(mDeps.openRawSocket6(fwmark));
+        } catch (IOException e) {
+            tunFd.close();
+            readSock6.close();
+            throw new IOException("Open raw socket failed: " + e);
+        }
+
+        final int ifaceIndex = mDeps.getInterfaceIndex(iface);
+        if (ifaceIndex == INVALID_IFINDEX) {
+            tunFd.close();
+            readSock6.close();
+            writeSock6.close();
+            throw new IOException("Fail to get interface index for interface " + iface);
+        }
+
+        // Start translating packets to the new prefix.
+        try {
+            mDeps.addAnycastSetsockopt(writeSock6.getFileDescriptor(), v6, ifaceIndex);
+        } catch (IOException e) {
+            tunFd.close();
+            readSock6.close();
+            writeSock6.close();
+            throw new IOException("add anycast sockopt failed: " + e);
+        }
+
+        // Update our packet socket filter to reflect the new 464xlat IP address.
+        try {
+            mDeps.configurePacketSocket(readSock6.getFileDescriptor(), v6, ifaceIndex);
+        } catch (IOException e) {
+            tunFd.close();
+            readSock6.close();
+            writeSock6.close();
+            throw new IOException("configure packet socket failed: " + e);
+        }
+
+        // [5] Start clatd.
+        try {
+            mPid = mDeps.startClatd(tunFd.getFileDescriptor(), readSock6.getFileDescriptor(),
+                    writeSock6.getFileDescriptor(), iface, pfx96, v4, v6);
+            mIface = iface;
+            mNat64Prefix = pfx96;
+            mXlatLocalAddress4 = v4;
+            mXlatLocalAddress6 = v6;
+        } catch (IOException e) {
+            throw new IOException("Error start clatd on " + iface + ": " + e);
+        } finally {
+            tunFd.close();
+            readSock6.close();
+            writeSock6.close();
+        }
+
+        return v6;
+    }
+
+    /**
+     * Stop clatd
+     */
+    public void clatStop() throws IOException {
+        if (mPid == INVALID_PID) {
+            throw new IOException("Clatd has not started");
+        }
+        Log.i(TAG, "Stopping clatd pid=" + mPid + " on " + mIface);
+
+        mDeps.stopClatd(mIface, mNat64Prefix, mXlatLocalAddress4, mXlatLocalAddress6, mPid);
+        // TODO: remove setIptablesDropRule
+
+        Log.i(TAG, "clatd on " + mIface + " stopped");
+
+        mIface = null;
+        mNat64Prefix = null;
+        mXlatLocalAddress4 = null;
+        mXlatLocalAddress6 = null;
+        mPid = INVALID_PID;
+    }
+
+    private static native String native_selectIpv4Address(String v4addr, int prefixlen)
+            throws IOException;
+    private static native String native_generateIpv6Address(String iface, String v4,
+            String prefix64) throws IOException;
+    private static native int native_createTunInterface(String tuniface) throws IOException;
+    private static native int native_detectMtu(String platSubnet, int platSuffix, int mark)
+            throws IOException;
+    private static native int native_openPacketSocket() throws IOException;
+    private static native int native_openRawSocket6(int mark) throws IOException;
+    private static native void native_addAnycastSetsockopt(FileDescriptor sock, String v6,
+            int ifindex) throws IOException;
+    private static native void native_configurePacketSocket(FileDescriptor sock, String v6,
+            int ifindex) throws IOException;
+    private static native int native_startClatd(FileDescriptor tunfd, FileDescriptor readsock6,
+            FileDescriptor writesock6, String iface, String pfx96, String v4, String v6)
+            throws IOException;
+    private static native void native_stopClatd(String iface, String pfx96, String v4, String v6,
+            int pid) throws IOException;
+}
diff --git a/service/src/com/android/server/connectivity/DscpPolicyTracker.java b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
new file mode 100644
index 0000000..43cfc8f
--- /dev/null
+++ b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
@@ -0,0 +1,271 @@
+/*
+ * 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.server.connectivity;
+
+import static android.net.DscpPolicy.STATUS_DELETED;
+import static android.net.DscpPolicy.STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
+import static android.net.DscpPolicy.STATUS_POLICY_NOT_FOUND;
+import static android.net.DscpPolicy.STATUS_SUCCESS;
+import static android.system.OsConstants.ETH_P_ALL;
+
+import android.annotation.NonNull;
+import android.net.DscpPolicy;
+import android.os.RemoteException;
+import android.system.ErrnoException;
+import android.util.Log;
+import android.util.SparseIntArray;
+
+import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.TcUtils;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.NetworkInterface;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * DscpPolicyTracker has a single entry point from ConnectivityService handler.
+ * This guarantees that all code runs on the same thread and no locking is needed.
+ */
+public class DscpPolicyTracker {
+    // After tethering and clat priorities.
+    static final short PRIO_DSCP = 5;
+
+    private static final String TAG = DscpPolicyTracker.class.getSimpleName();
+    private static final String PROG_PATH =
+            "/sys/fs/bpf/prog_dscp_policy_schedcls_set_dscp";
+    // Name is "map + *.o + map_name + map". Can probably shorten this
+    private static final String IPV4_POLICY_MAP_PATH = makeMapPath(
+            "dscp_policy_ipv4_dscp_policies");
+    private static final String IPV6_POLICY_MAP_PATH = makeMapPath(
+            "dscp_policy_ipv6_dscp_policies");
+    private static final int MAX_POLICIES = 16;
+
+    private static String makeMapPath(String which) {
+        return "/sys/fs/bpf/map_" + which + "_map";
+    }
+
+    private Set<String> mAttachedIfaces;
+
+    private final BpfMap<Struct.U32, DscpPolicyValue> mBpfDscpIpv4Policies;
+    private final BpfMap<Struct.U32, DscpPolicyValue> mBpfDscpIpv6Policies;
+    private final SparseIntArray mPolicyIdToBpfMapIndex;
+
+    public DscpPolicyTracker() throws ErrnoException {
+        mAttachedIfaces = new HashSet<String>();
+
+        mPolicyIdToBpfMapIndex = new SparseIntArray(MAX_POLICIES);
+        mBpfDscpIpv4Policies = new BpfMap<Struct.U32, DscpPolicyValue>(IPV4_POLICY_MAP_PATH,
+                BpfMap.BPF_F_RDWR, Struct.U32.class, DscpPolicyValue.class);
+        mBpfDscpIpv6Policies = new BpfMap<Struct.U32, DscpPolicyValue>(IPV6_POLICY_MAP_PATH,
+                BpfMap.BPF_F_RDWR, Struct.U32.class, DscpPolicyValue.class);
+    }
+
+    private int getFirstFreeIndex() {
+        for (int i = 0; i < MAX_POLICIES; i++) {
+            if (mPolicyIdToBpfMapIndex.indexOfValue(i) < 0) return i;
+        }
+        return MAX_POLICIES;
+    }
+
+    private void sendStatus(NetworkAgentInfo nai, int policyId, int status) {
+        try {
+            nai.networkAgent.onDscpPolicyStatusUpdated(policyId, status);
+        } catch (RemoteException e) {
+            Log.d(TAG, "Failed update policy status: ", e);
+        }
+    }
+
+    private boolean matchesIpv4(DscpPolicy policy) {
+        return ((policy.getDestinationAddress() == null
+                       || policy.getDestinationAddress() instanceof Inet4Address)
+            && (policy.getSourceAddress() == null
+                        || policy.getSourceAddress() instanceof Inet4Address));
+    }
+
+    private boolean matchesIpv6(DscpPolicy policy) {
+        return ((policy.getDestinationAddress() == null
+                       || policy.getDestinationAddress() instanceof Inet6Address)
+            && (policy.getSourceAddress() == null
+                        || policy.getSourceAddress() instanceof Inet6Address));
+    }
+
+    private int addDscpPolicyInternal(DscpPolicy policy) {
+        // If there is no existing policy with a matching ID, and we are already at
+        // the maximum number of policies then return INSUFFICIENT_PROCESSING_RESOURCES.
+        final int existingIndex = mPolicyIdToBpfMapIndex.get(policy.getPolicyId(), -1);
+        if (existingIndex == -1 && mPolicyIdToBpfMapIndex.size() >= MAX_POLICIES) {
+            return STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
+        }
+
+        // Currently all classifiers are supported, if any are removed return
+        // STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED,
+        // and for any other generic error STATUS_REQUEST_DECLINED
+
+        int addIndex = 0;
+        // If a policy with a matching ID exists, replace it, otherwise use the next free
+        // index for the policy.
+        if (existingIndex != -1) {
+            addIndex = mPolicyIdToBpfMapIndex.get(policy.getPolicyId());
+        } else {
+            addIndex = getFirstFreeIndex();
+        }
+
+        try {
+            mPolicyIdToBpfMapIndex.put(policy.getPolicyId(), addIndex);
+
+            // Add v4 policy to mBpfDscpIpv4Policies if source and destination address
+            // are both null or if they are both instances of Inet6Address.
+            if (matchesIpv4(policy)) {
+                mBpfDscpIpv4Policies.insertOrReplaceEntry(
+                        new Struct.U32(addIndex),
+                        new DscpPolicyValue(policy.getSourceAddress(),
+                            policy.getDestinationAddress(),
+                            policy.getSourcePort(), policy.getDestinationPortRange(),
+                            (short) policy.getProtocol(), (short) policy.getDscpValue()));
+            }
+
+            // Add v6 policy to mBpfDscpIpv6Policies if source and destination address
+            // are both null or if they are both instances of Inet6Address.
+            if (matchesIpv6(policy)) {
+                mBpfDscpIpv6Policies.insertOrReplaceEntry(
+                        new Struct.U32(addIndex),
+                        new DscpPolicyValue(policy.getSourceAddress(),
+                                policy.getDestinationAddress(),
+                                policy.getSourcePort(), policy.getDestinationPortRange(),
+                                (short) policy.getProtocol(), (short) policy.getDscpValue()));
+            }
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to insert policy into map: ", e);
+            return STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
+        }
+
+        return STATUS_SUCCESS;
+    }
+
+    /**
+     * Add the provided DSCP policy to the bpf map. Attach bpf program dscp_policy to iface
+     * if not already attached. Response will be sent back to nai with status.
+     *
+     * STATUS_SUCCESS - if policy was added successfully
+     * STATUS_INSUFFICIENT_PROCESSING_RESOURCES - if max policies were already set
+     */
+    public void addDscpPolicy(NetworkAgentInfo nai, DscpPolicy policy) {
+        if (!mAttachedIfaces.contains(nai.linkProperties.getInterfaceName())) {
+            if (!attachProgram(nai.linkProperties.getInterfaceName())) {
+                Log.e(TAG, "Unable to attach program");
+                sendStatus(nai, policy.getPolicyId(), STATUS_INSUFFICIENT_PROCESSING_RESOURCES);
+                return;
+            }
+        }
+
+        int status = addDscpPolicyInternal(policy);
+        sendStatus(nai, policy.getPolicyId(), status);
+    }
+
+    private void removePolicyFromMap(NetworkAgentInfo nai, int policyId, int index) {
+        int status = STATUS_POLICY_NOT_FOUND;
+        try {
+            mBpfDscpIpv4Policies.replaceEntry(new Struct.U32(index), DscpPolicyValue.NONE);
+            mBpfDscpIpv6Policies.replaceEntry(new Struct.U32(index), DscpPolicyValue.NONE);
+            status = STATUS_DELETED;
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to delete policy from map: ", e);
+        }
+
+        sendStatus(nai, policyId, status);
+    }
+
+    /**
+     * Remove specified DSCP policy and detach program if no other policies are active.
+     */
+    public void removeDscpPolicy(NetworkAgentInfo nai, int policyId) {
+        if (!mAttachedIfaces.contains(nai.linkProperties.getInterfaceName())) {
+            // Nothing to remove since program is not attached. Send update back for policy id.
+            sendStatus(nai, policyId, STATUS_POLICY_NOT_FOUND);
+            return;
+        }
+
+        if (mPolicyIdToBpfMapIndex.get(policyId, -1) != -1) {
+            removePolicyFromMap(nai, policyId, mPolicyIdToBpfMapIndex.get(policyId));
+            mPolicyIdToBpfMapIndex.delete(policyId);
+        }
+
+        // TODO: detach should only occur if no more policies are present on the nai's iface.
+        if (mPolicyIdToBpfMapIndex.size() == 0) {
+            detachProgram(nai.linkProperties.getInterfaceName());
+        }
+    }
+
+    /**
+     * Remove all DSCP policies and detach program.
+     */
+    // TODO: Remove all should only remove policies from corresponding nai iface.
+    public void removeAllDscpPolicies(NetworkAgentInfo nai) {
+        if (!mAttachedIfaces.contains(nai.linkProperties.getInterfaceName())) {
+            // Nothing to remove since program is not attached. Send update for policy
+            // id 0. The status update must contain a policy ID, and 0 is an invalid id.
+            sendStatus(nai, 0, STATUS_SUCCESS);
+            return;
+        }
+
+        for (int i = 0; i < mPolicyIdToBpfMapIndex.size(); i++) {
+            removePolicyFromMap(nai, mPolicyIdToBpfMapIndex.keyAt(i),
+                    mPolicyIdToBpfMapIndex.valueAt(i));
+        }
+        mPolicyIdToBpfMapIndex.clear();
+
+        // Can detach program since no policies are active.
+        detachProgram(nai.linkProperties.getInterfaceName());
+    }
+
+    /**
+     * Attach BPF program
+     */
+    private boolean attachProgram(@NonNull String iface) {
+        // TODO: attach needs to be per iface not program.
+
+        try {
+            NetworkInterface netIface = NetworkInterface.getByName(iface);
+            TcUtils.tcFilterAddDevBpf(netIface.getIndex(), false, PRIO_DSCP, (short) ETH_P_ALL,
+                    PROG_PATH);
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to attach to TC on " + iface + ": " + e);
+            return false;
+        }
+        mAttachedIfaces.add(iface);
+        return true;
+    }
+
+    /**
+     * Detach BPF program
+     */
+    public void detachProgram(@NonNull String iface) {
+        try {
+            NetworkInterface netIface = NetworkInterface.getByName(iface);
+            if (netIface != null) {
+                TcUtils.tcFilterDelDev(netIface.getIndex(), false, PRIO_DSCP, (short) ETH_P_ALL);
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to detach to TC on " + iface + ": " + e);
+        }
+        mAttachedIfaces.remove(iface);
+    }
+}
diff --git a/service/src/com/android/server/connectivity/DscpPolicyValue.java b/service/src/com/android/server/connectivity/DscpPolicyValue.java
new file mode 100644
index 0000000..cb40306
--- /dev/null
+++ b/service/src/com/android/server/connectivity/DscpPolicyValue.java
@@ -0,0 +1,180 @@
+/*
+ * 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.server.connectivity;
+
+import android.util.Log;
+import android.util.Range;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/** Value type for DSCP setting and rewriting to DSCP policy BPF maps. */
+public class DscpPolicyValue extends Struct {
+    private static final String TAG = DscpPolicyValue.class.getSimpleName();
+
+    // TODO: add the interface index.
+    @Field(order = 0, type = Type.ByteArray, arraysize = 16)
+    public final byte[] src46;
+
+    @Field(order = 1, type = Type.ByteArray, arraysize = 16)
+    public final byte[] dst46;
+
+    @Field(order = 2, type = Type.UBE16)
+    public final int srcPort;
+
+    @Field(order = 3, type = Type.UBE16)
+    public final int dstPortStart;
+
+    @Field(order = 4, type = Type.UBE16)
+    public final int dstPortEnd;
+
+    @Field(order = 5, type = Type.U8)
+    public final short proto;
+
+    @Field(order = 6, type = Type.U8)
+    public final short dscp;
+
+    @Field(order = 7, type = Type.U8, padding = 3)
+    public final short mask;
+
+    private static final int SRC_IP_MASK = 0x1;
+    private static final int DST_IP_MASK = 0x02;
+    private static final int SRC_PORT_MASK = 0x4;
+    private static final int DST_PORT_MASK = 0x8;
+    private static final int PROTO_MASK = 0x10;
+
+    private boolean ipEmpty(final byte[] ip) {
+        for (int i = 0; i < ip.length; i++) {
+            if (ip[i] != 0) return false;
+        }
+        return true;
+    }
+
+    private byte[] toIpv4MappedAddressBytes(InetAddress ia) {
+        final byte[] addr6 = new byte[16];
+        if (ia != null) {
+            final byte[] addr4 = ia.getAddress();
+            addr6[10] = (byte) 0xff;
+            addr6[11] = (byte) 0xff;
+            addr6[12] = addr4[0];
+            addr6[13] = addr4[1];
+            addr6[14] = addr4[2];
+            addr6[15] = addr4[3];
+        }
+        return addr6;
+    }
+
+    private byte[] toAddressField(InetAddress addr) {
+        if (addr == null) {
+            return EMPTY_ADDRESS_FIELD;
+        } else if (addr instanceof Inet4Address) {
+            return toIpv4MappedAddressBytes(addr);
+        } else {
+            return addr.getAddress();
+        }
+    }
+
+    private static final byte[] EMPTY_ADDRESS_FIELD =
+            InetAddress.parseNumericAddress("::").getAddress();
+
+    private short makeMask(final byte[] src46, final byte[] dst46, final int srcPort,
+            final int dstPortStart, final short proto, final short dscp) {
+        short mask = 0;
+        if (src46 != EMPTY_ADDRESS_FIELD) {
+            mask |= SRC_IP_MASK;
+        }
+        if (dst46 != EMPTY_ADDRESS_FIELD) {
+            mask |=  DST_IP_MASK;
+        }
+        if (srcPort != -1) {
+            mask |=  SRC_PORT_MASK;
+        }
+        if (dstPortStart != -1 && dstPortEnd != -1) {
+            mask |=  DST_PORT_MASK;
+        }
+        if (proto != -1) {
+            mask |=  PROTO_MASK;
+        }
+        return mask;
+    }
+
+    // This constructor is necessary for BpfMap#getValue since all values must be
+    // in the constructor.
+    public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int srcPort,
+            final int dstPortStart, final int dstPortEnd, final short proto,
+            final short dscp) {
+        this.src46 = toAddressField(src46);
+        this.dst46 = toAddressField(dst46);
+
+        // These params need to be stored as 0 because uints are used in BpfMap.
+        // If they are -1 BpfMap write will throw errors.
+        this.srcPort = srcPort != -1 ? srcPort : 0;
+        this.dstPortStart = dstPortStart != -1 ? dstPortStart : 0;
+        this.dstPortEnd = dstPortEnd != -1 ? dstPortEnd : 0;
+        this.proto = proto != -1 ? proto : 0;
+
+        this.dscp = dscp;
+        // Use member variables for IP since byte[] is needed and api variables for everything else
+        // so -1 is passed into mask if parameter is not present.
+        this.mask = makeMask(this.src46, this.dst46, srcPort, dstPortStart, proto, dscp);
+    }
+
+    public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int srcPort,
+            final Range<Integer> dstPort, final short proto,
+            final short dscp) {
+        this(src46, dst46, srcPort, dstPort != null ? dstPort.getLower() : -1,
+                dstPort != null ? dstPort.getUpper() : -1, proto, dscp);
+    }
+
+    public static final DscpPolicyValue NONE = new DscpPolicyValue(
+            null /* src46 */, null /* dst46 */, -1 /* srcPort */,
+            -1 /* dstPortStart */, -1 /* dstPortEnd */, (short) -1 /* proto */,
+            (short) 0 /* dscp */);
+
+    @Override
+    public String toString() {
+        String srcIpString = "empty";
+        String dstIpString = "empty";
+
+        // Separate try/catch for IP's so it's easier to debug.
+        try {
+            srcIpString = InetAddress.getByAddress(src46).getHostAddress();
+        }  catch (UnknownHostException e) {
+            Log.e(TAG, "Invalid SRC IP address", e);
+        }
+
+        try {
+            dstIpString = InetAddress.getByAddress(src46).getHostAddress();
+        }  catch (UnknownHostException e) {
+            Log.e(TAG, "Invalid DST IP address", e);
+        }
+
+        try {
+            return String.format(
+                    "src46: %s, dst46: %s, srcPort: %d, dstPortStart: %d, dstPortEnd: %d,"
+                    + " protocol: %d, dscp %s", srcIpString, dstIpString, srcPort, dstPortStart,
+                    dstPortEnd, proto, dscp);
+        } catch (IllegalArgumentException e) {
+            return String.format("String format error: " + e);
+        }
+    }
+}
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index b7f3ed9..1a7248a 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -24,6 +24,7 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.net.CaptivePortalData;
+import android.net.DscpPolicy;
 import android.net.IDnsResolver;
 import android.net.INetd;
 import android.net.INetworkAgent;
@@ -700,6 +701,24 @@
             mHandler.obtainMessage(NetworkAgent.EVENT_LINGER_DURATION_CHANGED,
                     new Pair<>(NetworkAgentInfo.this, durationMs)).sendToTarget();
         }
+
+        @Override
+        public void sendAddDscpPolicy(final DscpPolicy policy) {
+            mHandler.obtainMessage(NetworkAgent.EVENT_ADD_DSCP_POLICY,
+                    new Pair<>(NetworkAgentInfo.this, policy)).sendToTarget();
+        }
+
+        @Override
+        public void sendRemoveDscpPolicy(final int policyId) {
+            mHandler.obtainMessage(NetworkAgent.EVENT_REMOVE_DSCP_POLICY,
+                    new Pair<>(NetworkAgentInfo.this, policyId)).sendToTarget();
+        }
+
+        @Override
+        public void sendRemoveAllDscpPolicies() {
+            mHandler.obtainMessage(NetworkAgent.EVENT_REMOVE_ALL_DSCP_POLICIES,
+                    new Pair<>(NetworkAgentInfo.this, null)).sendToTarget();
+        }
     }
 
     /**
diff --git a/service/src/com/android/server/connectivity/UidRangeUtils.java b/service/src/com/android/server/connectivity/UidRangeUtils.java
new file mode 100644
index 0000000..7318296
--- /dev/null
+++ b/service/src/com/android/server/connectivity/UidRangeUtils.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2022 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.server.connectivity;
+
+import android.annotation.NonNull;
+import android.net.UidRange;
+import android.util.ArraySet;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Utility class for UidRange
+ *
+ * @hide
+ */
+public final class UidRangeUtils {
+    /**
+     * Check if given uid range set is within the uid range
+     * @param uids uid range in which uidRangeSet is checked to be in range.
+     * @param uidRangeSet uid range set to be be checked if it is in range of uids
+     * @return true uidRangeSet is in the range of uids
+     * @hide
+     */
+    public static boolean isRangeSetInUidRange(@NonNull UidRange uids,
+            @NonNull Set<UidRange> uidRangeSet) {
+        Objects.requireNonNull(uids);
+        Objects.requireNonNull(uidRangeSet);
+        if (uidRangeSet.size() == 0) {
+            return true;
+        }
+        for (UidRange range : uidRangeSet) {
+            if (!uids.contains(range.start) || !uids.contains(range.stop)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Remove given uid ranges set from a uid range
+     * @param uids uid range from which uidRangeSet will be removed
+     * @param uidRangeSet uid range set to be removed from uids.
+     * WARNING : This function requires the UidRanges in uidRangeSet to be disjoint
+     * WARNING : This function requires the arrayset to be iterated in increasing order of the
+     *                    ranges. Today this is provided by the iteration order stability of
+     *                    ArraySet, and the fact that the code creating this ArraySet always
+     *                    creates it in increasing order.
+     * Note : if any of the above is not satisfied this function throws IllegalArgumentException
+     * TODO : remove these limitations
+     * @hide
+     */
+    public static ArraySet<UidRange> removeRangeSetFromUidRange(@NonNull UidRange uids,
+            @NonNull ArraySet<UidRange> uidRangeSet) {
+        Objects.requireNonNull(uids);
+        Objects.requireNonNull(uidRangeSet);
+        final ArraySet<UidRange> filteredRangeSet = new ArraySet<UidRange>();
+        if (uidRangeSet.size() == 0) {
+            filteredRangeSet.add(uids);
+            return filteredRangeSet;
+        }
+
+        int start = uids.start;
+        UidRange previousRange = null;
+        for (UidRange uidRange : uidRangeSet) {
+            if (previousRange != null) {
+                if (previousRange.stop > uidRange.start) {
+                    throw new IllegalArgumentException("UID ranges are not increasing order");
+                }
+            }
+            if (uidRange.start > start) {
+                filteredRangeSet.add(new UidRange(start, uidRange.start - 1));
+                start = uidRange.stop + 1;
+            } else if (uidRange.start == start) {
+                start = uidRange.stop + 1;
+            }
+            previousRange = uidRange;
+        }
+        if (start < uids.stop) {
+            filteredRangeSet.add(new UidRange(start, uids.stop));
+        }
+        return filteredRangeSet;
+    }
+
+    /**
+     * Compare if the given UID range sets have overlapping uids
+     * @param uidRangeSet1 first uid range set to check for overlap
+     * @param uidRangeSet2 second uid range set to check for overlap
+     * @hide
+     */
+    public static boolean doesRangeSetOverlap(@NonNull Set<UidRange> uidRangeSet1,
+            @NonNull Set<UidRange> uidRangeSet2) {
+        Objects.requireNonNull(uidRangeSet1);
+        Objects.requireNonNull(uidRangeSet2);
+
+        if (uidRangeSet1.size() == 0 || uidRangeSet2.size() == 0) {
+            return false;
+        }
+        for (UidRange range1 : uidRangeSet1) {
+            for (UidRange range2 : uidRangeSet2) {
+                if (range1.contains(range2.start) || range1.contains(range2.stop)
+                        || range2.contains(range1.start) || range2.contains(range1.stop)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Convert a list of uid to set of UidRanges.
+     * @param uids list of uids
+     * @return set of UidRanges
+     * @hide
+     */
+    public static ArraySet<UidRange> convertListToUidRange(@NonNull List<Integer> uids) {
+        Objects.requireNonNull(uids);
+        final ArraySet<UidRange> uidRangeSet = new ArraySet<UidRange>();
+        if (uids.size() == 0) {
+            return uidRangeSet;
+        }
+        List<Integer> uidsNew = new ArrayList<>(uids);
+        Collections.sort(uidsNew);
+        int start = uidsNew.get(0);
+        int stop = start;
+
+        for (Integer i : uidsNew) {
+            if (i <= stop + 1) {
+                stop = i;
+            } else {
+                uidRangeSet.add(new UidRange(start, stop));
+                start = i;
+                stop = i;
+            }
+        }
+        uidRangeSet.add(new UidRange(start, stop));
+        return uidRangeSet;
+    }
+}
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index f1897f5..acf04bf 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -122,3 +122,25 @@
         "framework-res",
     ],
 }
+
+// Defaults for tests that want to run in mainline-presubmit.
+// Not widely used because many of our tests have AndroidTest.xml files and
+// use the mainline-param config-descriptor metadata in AndroidTest.xml.
+
+// test_mainline_modules is an array of strings. Each element in the array is a list of modules
+// separated by "+". The modules in this list must be in alphabetical order.
+// See SuiteModuleLoader.java.
+// TODO: why are the modules separated by + instead of being separate entries in the array?
+mainline_presubmit_modules = [
+        "CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex",
+]
+
+cc_defaults {
+    name: "connectivity-mainline-presubmit-cc-defaults",
+    test_mainline_modules: mainline_presubmit_modules,
+}
+
+java_defaults {
+    name: "connectivity-mainline-presubmit-java-defaults",
+    test_mainline_modules: mainline_presubmit_modules,
+}
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index 9cc001d..bea00a9 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -23,11 +23,6 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_EIMS;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
@@ -39,9 +34,16 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_BANDWIDTH;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_LATENCY;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_2;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_3;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_4;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5;
 import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION;
 import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
 import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
@@ -57,6 +59,7 @@
 import static com.android.modules.utils.build.SdkLevel.isAtLeastR;
 import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 import static com.android.testutils.MiscAsserts.assertEmpty;
 import static com.android.testutils.MiscAsserts.assertThrows;
 import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
@@ -414,6 +417,31 @@
         assertFalse(nr.satisfiedByNetworkCapabilities(new NetworkCapabilities()));
     }
 
+    @Test @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+    public void testPrioritizeLatencyAndBandwidth() {
+        NetworkCapabilities netCap = new NetworkCapabilities();
+        netCap.addCapability(NET_CAPABILITY_PRIORITIZE_LATENCY);
+        netCap.addCapability(NET_CAPABILITY_NOT_METERED);
+        netCap.maybeMarkCapabilitiesRestricted();
+        assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+        netCap = new NetworkCapabilities();
+        netCap.addCapability(NET_CAPABILITY_PRIORITIZE_LATENCY);
+        netCap.removeCapability(NET_CAPABILITY_NOT_METERED);
+        netCap.maybeMarkCapabilitiesRestricted();
+        assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+
+        netCap = new NetworkCapabilities();
+        netCap.addCapability(NET_CAPABILITY_PRIORITIZE_BANDWIDTH);
+        netCap.addCapability(NET_CAPABILITY_NOT_METERED);
+        netCap.maybeMarkCapabilitiesRestricted();
+        assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+        netCap = new NetworkCapabilities();
+        netCap.addCapability(NET_CAPABILITY_PRIORITIZE_BANDWIDTH);
+        netCap.removeCapability(NET_CAPABILITY_NOT_METERED);
+        netCap.maybeMarkCapabilitiesRestricted();
+        assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+    }
+
     @Test @IgnoreUpTo(Build.VERSION_CODES.R)
     public void testOemPrivate() {
         NetworkCapabilities nc = new NetworkCapabilities();
@@ -788,79 +816,80 @@
         } catch (IllegalStateException expected) { }
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.S)
-    public void testEnterpriseCapabilitySubLevel() {
+    @Test @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+    public void testEnterpriseId() {
         final NetworkCapabilities nc1 = new NetworkCapabilities.Builder()
                 .addCapability(NET_CAPABILITY_ENTERPRISE)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
+                .addEnterpriseId(NET_ENTERPRISE_ID_1)
                 .build();
-        assertEquals(1, nc1.getEnterpriseCapabilitySubLevels().length);
-        assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1,
-                nc1.getEnterpriseCapabilitySubLevels()[0]);
+        assertEquals(1, nc1.getEnterpriseIds().length);
+        assertEquals(NET_ENTERPRISE_ID_1,
+                nc1.getEnterpriseIds()[0]);
         final NetworkCapabilities nc2 = new NetworkCapabilities.Builder()
                 .addCapability(NET_CAPABILITY_ENTERPRISE)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2)
+                .addEnterpriseId(NET_ENTERPRISE_ID_1)
+                .addEnterpriseId(NET_ENTERPRISE_ID_2)
                 .build();
-        assertEquals(2, nc2.getEnterpriseCapabilitySubLevels().length);
-        assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1,
-                nc2.getEnterpriseCapabilitySubLevels()[0]);
-        assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2,
-                nc2.getEnterpriseCapabilitySubLevels()[1]);
+        assertEquals(2, nc2.getEnterpriseIds().length);
+        assertEquals(NET_ENTERPRISE_ID_1,
+                nc2.getEnterpriseIds()[0]);
+        assertEquals(NET_ENTERPRISE_ID_2,
+                nc2.getEnterpriseIds()[1]);
         final NetworkCapabilities nc3 = new NetworkCapabilities.Builder()
                 .addCapability(NET_CAPABILITY_ENTERPRISE)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5)
+                .addEnterpriseId(NET_ENTERPRISE_ID_1)
+                .addEnterpriseId(NET_ENTERPRISE_ID_2)
+                .addEnterpriseId(NET_ENTERPRISE_ID_3)
+                .addEnterpriseId(NET_ENTERPRISE_ID_4)
+                .addEnterpriseId(NET_ENTERPRISE_ID_5)
                 .build();
-        assertEquals(5, nc3.getEnterpriseCapabilitySubLevels().length);
-        assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1,
-                nc3.getEnterpriseCapabilitySubLevels()[0]);
-        assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2,
-                nc3.getEnterpriseCapabilitySubLevels()[1]);
-        assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3,
-                nc3.getEnterpriseCapabilitySubLevels()[2]);
-        assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4,
-                nc3.getEnterpriseCapabilitySubLevels()[3]);
-        assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5,
-                nc3.getEnterpriseCapabilitySubLevels()[4]);
+        assertEquals(5, nc3.getEnterpriseIds().length);
+        assertEquals(NET_ENTERPRISE_ID_1,
+                nc3.getEnterpriseIds()[0]);
+        assertEquals(NET_ENTERPRISE_ID_2,
+                nc3.getEnterpriseIds()[1]);
+        assertEquals(NET_ENTERPRISE_ID_3,
+                nc3.getEnterpriseIds()[2]);
+        assertEquals(NET_ENTERPRISE_ID_4,
+                nc3.getEnterpriseIds()[3]);
+        assertEquals(NET_ENTERPRISE_ID_5,
+                nc3.getEnterpriseIds()[4]);
 
         final Class<IllegalArgumentException> illegalArgumentExceptionClass =
                 IllegalArgumentException.class;
         assertThrows(illegalArgumentExceptionClass, () -> new NetworkCapabilities.Builder()
-                .addEnterpriseCapabilitySubLevel(6)
+                .addEnterpriseId(6)
                 .build());
         assertThrows(illegalArgumentExceptionClass, () -> new NetworkCapabilities.Builder()
-                .removeEnterpriseCapabilitySubLevel(6)
+                .removeEnterpriseId(6)
                 .build());
 
         final Class<IllegalStateException> illegalStateException =
                 IllegalStateException.class;
         assertThrows(illegalStateException, () -> new NetworkCapabilities.Builder()
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
+                .addEnterpriseId(NET_ENTERPRISE_ID_1)
                 .build());
 
         final NetworkCapabilities nc4 = new NetworkCapabilities.Builder()
                 .addCapability(NET_CAPABILITY_ENTERPRISE)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2)
-                .removeEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
-                .removeEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2)
+                .addEnterpriseId(NET_ENTERPRISE_ID_1)
+                .addEnterpriseId(NET_ENTERPRISE_ID_2)
+                .removeEnterpriseId(NET_ENTERPRISE_ID_1)
+                .removeEnterpriseId(NET_ENTERPRISE_ID_2)
                 .build();
-        assertEquals(0, nc4.getEnterpriseCapabilitySubLevels().length);
+        assertEquals(1, nc4.getEnterpriseIds().length);
+        assertTrue(nc4.hasEnterpriseId(NET_ENTERPRISE_ID_1));
 
         final NetworkCapabilities nc5 = new NetworkCapabilities.Builder()
                 .addCapability(NET_CAPABILITY_CBS)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
-                .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2)
-                .removeEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
-                .removeEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2)
+                .addEnterpriseId(NET_ENTERPRISE_ID_1)
+                .addEnterpriseId(NET_ENTERPRISE_ID_2)
+                .removeEnterpriseId(NET_ENTERPRISE_ID_1)
+                .removeEnterpriseId(NET_ENTERPRISE_ID_2)
                 .build();
 
         assertTrue(nc4.satisfiedByNetworkCapabilities(nc1));
-        assertFalse(nc1.satisfiedByNetworkCapabilities(nc4));
+        assertTrue(nc1.satisfiedByNetworkCapabilities(nc4));
 
         assertFalse(nc3.satisfiedByNetworkCapabilities(nc2));
         assertTrue(nc2.satisfiedByNetworkCapabilities(nc3));
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 916b566..5778b0d 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
@@ -36,6 +36,7 @@
 
 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
 import static com.android.testutils.Cleanup.testAndCleanup;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -71,7 +72,6 @@
 import android.net.VpnTransportInfo;
 import android.net.cts.util.CtsNetUtils;
 import android.net.wifi.WifiManager;
-import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.ParcelFileDescriptor;
@@ -830,7 +830,7 @@
                                 .getCaps().getUnderlyingNetworks())));
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.S)
+    @Test @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
     public void testChangeUnderlyingNetworks() throws Exception {
         assumeTrue(supportedHardware());
         assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 07a319d..e979a3b 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -49,6 +49,7 @@
         "FrameworksNetCommonTests",
         "core-tests-support",
         "cts-net-utils",
+        "CtsNetTestsNonUpdatableLib",
         "ctstestrunner-axt",
         "junit",
         "junit-params",
diff --git a/tests/cts/net/native/Android.bp b/tests/cts/net/native/Android.bp
index 1d1c18e..153ff51 100644
--- a/tests/cts/net/native/Android.bp
+++ b/tests/cts/net/native/Android.bp
@@ -43,6 +43,7 @@
     static_libs: [
         "libbpf_android",
         "libgtest",
+        "libmodules-utils-build",
     ],
 
     // Tag this module as a cts test artifact
diff --git a/tests/cts/net/native/src/BpfCompatTest.cpp b/tests/cts/net/native/src/BpfCompatTest.cpp
index 874bad4..97ecb9e 100644
--- a/tests/cts/net/native/src/BpfCompatTest.cpp
+++ b/tests/cts/net/native/src/BpfCompatTest.cpp
@@ -21,6 +21,8 @@
 
 #include <gtest/gtest.h>
 
+#include "android-modules-utils/sdk_level.h"
+
 #include "libbpf_android.h"
 
 using namespace android::bpf;
@@ -33,11 +35,17 @@
   EXPECT_EQ(28, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
 }
 
-TEST(BpfTest, bpfStructSizeTest) {
+TEST(BpfTest, bpfStructSizeTestPreT) {
+  if (android::modules::sdklevel::IsAtLeastT()) GTEST_SKIP() << "T+ device.";
   doBpfStructSizeTest("/system/etc/bpf/netd.o");
   doBpfStructSizeTest("/system/etc/bpf/clatd.o");
 }
 
+TEST(BpfTest, bpfStructSizeTest) {
+  doBpfStructSizeTest("/system/etc/bpf/gpu_mem.o");
+  doBpfStructSizeTest("/system/etc/bpf/time_in_state.o");
+}
+
 int main(int argc, char **argv) {
   testing::InitGoogleTest(&argc, argv);
   return RUN_ALL_TESTS();
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 80c2db4..53e4ab7 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -168,6 +168,7 @@
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRuleKt;
+import com.android.testutils.DumpTestUtils;
 import com.android.testutils.RecorderCallback.CallbackEntry;
 import com.android.testutils.TestHttpServer;
 import com.android.testutils.TestNetworkTracker;
@@ -3021,6 +3022,13 @@
         }
     }
 
+    @Test
+    public void testDump() throws Exception {
+        final String dumpOutput = DumpTestUtils.dumpServiceWithShellPermission(
+                Context.CONNECTIVITY_SERVICE, "--short");
+        assertTrue(dumpOutput, dumpOutput.contains("Active default network"));
+    }
+
     private void unregisterRegisteredCallbacks() {
         for (NetworkCallback callback: mRegisteredCallbacks) {
             mCm.unregisterNetworkCallback(callback);
diff --git a/tests/cts/net/src/android/net/cts/DhcpOptionTest.kt b/tests/cts/net/src/android/net/cts/DhcpOptionTest.kt
index 1a62560..555dd87 100644
--- a/tests/cts/net/src/android/net/cts/DhcpOptionTest.kt
+++ b/tests/cts/net/src/android/net/cts/DhcpOptionTest.kt
@@ -16,11 +16,11 @@
 
 package android.net.cts
 
-import android.os.Build
 import android.net.DhcpOption
 import androidx.test.filters.SmallTest
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.SC_V2
 import org.junit.Assert.assertArrayEquals
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNull
@@ -28,7 +28,7 @@
 import org.junit.Test
 
 @SmallTest
-@IgnoreUpTo(Build.VERSION_CODES.S)
+@IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
 @RunWith(DevSdkIgnoreRunner::class)
 class DhcpOptionTest {
     private val DHCP_OPTION_TYPE: Byte = 2
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
index 4992795..c6fc38f 100644
--- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -25,6 +25,8 @@
 import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
 import static android.system.OsConstants.ETIMEDOUT;
 
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
@@ -43,7 +45,6 @@
 import android.net.NetworkRequest;
 import android.net.ParseException;
 import android.net.cts.util.CtsNetUtils;
-import android.os.Build;
 import android.os.CancellationSignal;
 import android.os.Handler;
 import android.os.Looper;
@@ -814,7 +815,7 @@
     }
 
     /** Verifies that DnsResolver.DnsException can be subclassed and its constructor re-used. */
-    @Test @IgnoreUpTo(Build.VERSION_CODES.S)
+    @Test @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
     public void testDnsExceptionConstructor() throws InterruptedException {
         class TestDnsException extends DnsResolver.DnsException {
             TestDnsException(int code, @Nullable Throwable cause) {
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
new file mode 100644
index 0000000..1f39fee
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -0,0 +1,522 @@
+/*
+ * 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 android.net.cts
+
+import android.net.cts.util.CtsNetUtils.TestNetworkCallback
+
+import android.app.Instrumentation
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.cts.util.CtsNetUtils
+import android.net.DscpPolicy
+import android.net.DscpPolicy.STATUS_DELETED
+import android.net.DscpPolicy.STATUS_SUCCESS
+import android.net.InetAddresses
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NetworkAgent
+import android.net.NetworkAgentConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
+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_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkRequest
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import android.net.RouteInfo
+import android.net.util.SocketUtils
+import android.os.Build
+import android.os.HandlerThread
+import android.os.Looper
+import android.platform.test.annotations.AppModeFull
+import android.system.Os
+import android.system.OsConstants
+import android.system.OsConstants.AF_INET
+import android.system.OsConstants.IPPROTO_IP
+import android.system.OsConstants.IPPROTO_UDP
+import android.system.OsConstants.SOCK_DGRAM
+import android.system.OsConstants.SOCK_NONBLOCK
+import android.util.Log
+import android.util.Range
+import androidx.test.InstrumentationRegistry
+import androidx.test.runner.AndroidJUnit4
+import com.android.modules.utils.build.SdkLevel
+import com.android.testutils.CompatUtil
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.OffsetFilter
+import com.android.testutils.assertParcelingIsLossless
+import com.android.testutils.runAsShell
+import com.android.testutils.SC_V2
+import com.android.testutils.TapPacketReader
+import com.android.testutils.TestableNetworkAgent
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnDscpPolicyStatusUpdated
+import com.android.testutils.TestableNetworkCallback
+import org.junit.After
+import org.junit.AfterClass
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.net.Inet4Address
+import java.net.Inet6Address
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import java.net.ServerSocket
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import java.util.UUID
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+import kotlin.concurrent.thread
+import kotlin.test.fail
+
+private const val MAX_PACKET_LENGTH = 1500
+
+private val instrumentation: Instrumentation
+    get() = InstrumentationRegistry.getInstrumentation()
+
+private const val TAG = "DscpPolicyTest"
+private const val PACKET_TIMEOUT_MS = 2_000L
+
+@AppModeFull(reason = "Instant apps cannot create test networks")
+@RunWith(AndroidJUnit4::class)
+class DscpPolicyTest {
+    @JvmField
+    @Rule
+    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
+
+    private val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
+    private val TEST_TARGET_IPV4_ADDR =
+            InetAddresses.parseNumericAddress("8.8.8.8") as Inet4Address
+
+    private val realContext = InstrumentationRegistry.getContext()
+    private val cm = realContext.getSystemService(ConnectivityManager::class.java)
+
+    private val agentsToCleanUp = mutableListOf<NetworkAgent>()
+    private val callbacksToCleanUp = mutableListOf<TestableNetworkCallback>()
+
+    private val handlerThread = HandlerThread(DscpPolicyTest::class.java.simpleName)
+
+    private lateinit var iface: TestNetworkInterface
+    private lateinit var tunNetworkCallback: TestNetworkCallback
+    private lateinit var reader: TapPacketReader
+
+    @Before
+    fun setUp() {
+        runAsShell(MANAGE_TEST_NETWORKS) {
+            val tnm = realContext.getSystemService(TestNetworkManager::class.java)
+
+            iface = tnm.createTunInterface( Array(1){ LinkAddress(LOCAL_IPV4_ADDRESS, 32) } )
+            assertNotNull(iface)
+        }
+
+        handlerThread.start()
+        reader = TapPacketReader(
+                handlerThread.threadHandler,
+                iface.fileDescriptor.fileDescriptor,
+                MAX_PACKET_LENGTH)
+        reader.startAsyncForTest()
+    }
+
+    @After
+    fun tearDown() {
+        agentsToCleanUp.forEach { it.unregister() }
+        callbacksToCleanUp.forEach { cm.unregisterNetworkCallback(it) }
+        reader.handler.post { reader.stop() }
+        Os.close(iface.fileDescriptor.fileDescriptor)
+        handlerThread.quitSafely()
+    }
+
+    private fun requestNetwork(request: NetworkRequest, callback: TestableNetworkCallback) {
+        cm.requestNetwork(request, callback)
+        callbacksToCleanUp.add(callback)
+    }
+
+    private fun makeTestNetworkRequest(specifier: String? = null): NetworkRequest {
+        return NetworkRequest.Builder()
+                .clearCapabilities()
+                .addCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                .addTransportType(TRANSPORT_TEST)
+                .also {
+                    if (specifier != null) {
+                        it.setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(specifier))
+                    }
+                }
+                .build()
+    }
+
+    private fun createConnectedNetworkAgent(
+        context: Context = realContext,
+        specifier: String? = iface.getInterfaceName(),
+    ): Pair<TestableNetworkAgent, TestableNetworkCallback> {
+        val callback = TestableNetworkCallback()
+        // Ensure this NetworkAgent is never unneeded by filing a request with its specifier.
+        requestNetwork(makeTestNetworkRequest(specifier = specifier), callback)
+
+        val nc = NetworkCapabilities().apply {
+            addTransportType(TRANSPORT_TEST)
+            removeCapability(NET_CAPABILITY_TRUSTED)
+            removeCapability(NET_CAPABILITY_INTERNET)
+            addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+            addCapability(NET_CAPABILITY_NOT_ROAMING)
+            addCapability(NET_CAPABILITY_NOT_VPN)
+            addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+            if (null != specifier) {
+                setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(specifier))
+            }
+        }
+        val lp = LinkProperties().apply {
+            addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+            addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
+            setInterfaceName(iface.getInterfaceName())
+        }
+        val config = NetworkAgentConfig.Builder().build()
+        val agent = TestableNetworkAgent(context, handlerThread.looper, nc, lp, config)
+        agentsToCleanUp.add(agent)
+
+        // Connect the agent and verify initial status callbacks.
+        runAsShell(MANAGE_TEST_NETWORKS) { agent.register() }
+        agent.markConnected()
+        agent.expectCallback<OnNetworkCreated>()
+        agent.expectSignalStrengths(intArrayOf())
+        agent.expectValidationBypassedStatus()
+        val network = agent.network ?: fail("Expected a non-null network")
+        return agent to callback
+    }
+
+    fun ByteArray.toHex(): String = joinToString(separator = "") {
+        eachByte -> "%02x".format(eachByte)
+    }
+
+    fun checkDscpValue(
+        agent : TestableNetworkAgent,
+        callback : TestableNetworkCallback,
+        dscpValue : Int = 0,
+        dstPort : Int = 0,
+    ) {
+        val testString = "test string"
+        val testPacket = ByteBuffer.wrap(testString.toByteArray(Charsets.UTF_8))
+        var packetFound = false
+
+        val socket = Os.socket(AF_INET, SOCK_DGRAM or SOCK_NONBLOCK, IPPROTO_UDP)
+        agent.network.bindSocket(socket)
+
+        val originalPacket = testPacket.readAsArray()
+        Os.sendto(socket, originalPacket, 0 /* bytesOffset */, originalPacket.size,
+                0 /* flags */, TEST_TARGET_IPV4_ADDR, dstPort)
+
+        Os.close(socket)
+        generateSequence { reader.poll(PACKET_TIMEOUT_MS) }.forEach { packet ->
+            val buffer = ByteBuffer.wrap(packet, 0, packet.size).order(ByteOrder.BIG_ENDIAN)
+            val ip_ver = buffer.get()
+            val tos = buffer.get()
+            val length = buffer.getShort()
+            val id = buffer.getShort()
+            val offset = buffer.getShort()
+            val ttl = buffer.get()
+            val ipType = buffer.get()
+            val checksum = buffer.getShort()
+
+            val ipAddr = ByteArray(4)
+            buffer.get(ipAddr)
+            val srcIp = Inet4Address.getByAddress(ipAddr);
+            buffer.get(ipAddr)
+            val dstIp = Inet4Address.getByAddress(ipAddr);
+            val packetSrcPort = buffer.getShort().toInt()
+            val packetDstPort = buffer.getShort().toInt()
+
+            // TODO: Add source port comparison.
+            if (srcIp == LOCAL_IPV4_ADDRESS && dstIp == TEST_TARGET_IPV4_ADDR &&
+                    packetDstPort == dstPort) {
+                assertEquals(dscpValue, (tos.toInt().shr(2)))
+                packetFound = true
+            }
+        }
+        assertTrue(packetFound)
+    }
+
+    fun doRemovePolicyTest(
+        agent : TestableNetworkAgent,
+        callback : TestableNetworkCallback,
+        policyId : Int
+    ) {
+        val portNumber = 1111 * policyId
+        agent.sendRemoveDscpPolicy(policyId)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(policyId, it.policyId)
+            assertEquals(STATUS_DELETED, it.status)
+            checkDscpValue(agent, callback, dstPort = portNumber)
+        }
+    }
+
+    @Test
+    fun testDscpPolicyAddPolicies(): Unit = createConnectedNetworkAgent().let { (agent, callback) ->
+        val policy = DscpPolicy.Builder(1, 1)
+                .setDestinationPortRange(Range(4444, 4444)).build()
+        agent.sendAddDscpPolicy(policy)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(STATUS_SUCCESS, it.status)
+        }
+
+        checkDscpValue(agent, callback, dscpValue = 1, dstPort = 4444)
+
+        agent.sendRemoveDscpPolicy(1)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(STATUS_DELETED, it.status)
+        }
+
+        val policy2 = DscpPolicy.Builder(1, 4)
+                .setDestinationPortRange(Range(5555, 5555)).setSourceAddress(LOCAL_IPV4_ADDRESS)
+                .setDestinationAddress(TEST_TARGET_IPV4_ADDR).setProtocol(IPPROTO_UDP).build()
+        agent.sendAddDscpPolicy(policy2)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(STATUS_SUCCESS, it.status)
+        }
+
+        checkDscpValue(agent, callback, dscpValue = 4, dstPort = 5555)
+
+        agent.sendRemoveDscpPolicy(1)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(STATUS_DELETED, it.status)
+        }
+    }
+
+    @Test
+    // Remove policies in the same order as addition.
+    fun testRemoveDscpPolicy_RemoveSameOrderAsAdd(): Unit = createConnectedNetworkAgent().let {
+                (agent, callback) ->
+        val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(1111, 1111)).build()
+        agent.sendAddDscpPolicy(policy)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(STATUS_SUCCESS, it.status)
+            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+        }
+
+        val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
+        agent.sendAddDscpPolicy(policy2)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(2, it.policyId)
+            assertEquals(STATUS_SUCCESS, it.status)
+            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+        }
+
+        val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
+        agent.sendAddDscpPolicy(policy3)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(3, it.policyId)
+            assertEquals(STATUS_SUCCESS, it.status)
+            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+        }
+
+        /* Remove Policies and check CE is no longer set */
+        doRemovePolicyTest(agent, callback, 1)
+        doRemovePolicyTest(agent, callback, 2)
+        doRemovePolicyTest(agent, callback, 3)
+    }
+
+    @Test
+    fun testRemoveDscpPolicy_RemoveImmediatelyAfterAdd(): Unit =
+            createConnectedNetworkAgent().let{ (agent, callback) ->
+        val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(1111, 1111)).build()
+        agent.sendAddDscpPolicy(policy)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(STATUS_SUCCESS, it.status)
+            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+        }
+        doRemovePolicyTest(agent, callback, 1)
+
+        val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
+        agent.sendAddDscpPolicy(policy2)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(2, it.policyId)
+            assertEquals(STATUS_SUCCESS, it.status)
+            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+        }
+        doRemovePolicyTest(agent, callback, 2)
+
+        val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
+        agent.sendAddDscpPolicy(policy3)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(3, it.policyId)
+            assertEquals(STATUS_SUCCESS, it.status)
+            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+        }
+        doRemovePolicyTest(agent, callback, 3)
+    }
+
+    @Test
+    // Remove policies in reverse order from addition.
+    fun testRemoveDscpPolicy_RemoveReverseOrder(): Unit =
+            createConnectedNetworkAgent().let { (agent, callback) ->
+        val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(1111, 1111)).build()
+        agent.sendAddDscpPolicy(policy)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(STATUS_SUCCESS, it.status)
+            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+        }
+
+        val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
+        agent.sendAddDscpPolicy(policy2)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(2, it.policyId)
+            assertEquals(STATUS_SUCCESS, it.status)
+            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+        }
+
+        val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
+        agent.sendAddDscpPolicy(policy3)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(3, it.policyId)
+            assertEquals(STATUS_SUCCESS, it.status)
+            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+        }
+
+        /* Remove Policies and check CE is no longer set */
+        doRemovePolicyTest(agent, callback, 3)
+        doRemovePolicyTest(agent, callback, 2)
+        doRemovePolicyTest(agent, callback, 1)
+    }
+
+    @Test
+    fun testRemoveDscpPolicy_InvalidPolicy(): Unit = createConnectedNetworkAgent().let {
+                (agent, callback) ->
+        agent.sendRemoveDscpPolicy(3)
+        // Is there something to add in TestableNetworkCallback to NOT expect a callback?
+        // Or should we send STATUS_DELETED in any case or a different STATUS?
+    }
+
+    @Test
+    fun testRemoveAllDscpPolicies(): Unit = createConnectedNetworkAgent().let { (agent, callback) ->
+        val policy = DscpPolicy.Builder(1, 1)
+                .setDestinationPortRange(Range(1111, 1111)).build()
+        agent.sendAddDscpPolicy(policy)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(STATUS_SUCCESS, it.status)
+            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+        }
+
+        val policy2 = DscpPolicy.Builder(2, 1)
+                .setDestinationPortRange(Range(2222, 2222)).build()
+        agent.sendAddDscpPolicy(policy2)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(2, it.policyId)
+            assertEquals(STATUS_SUCCESS, it.status)
+            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+        }
+
+        val policy3 = DscpPolicy.Builder(3, 1)
+                .setDestinationPortRange(Range(3333, 3333)).build()
+        agent.sendAddDscpPolicy(policy3)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(3, it.policyId)
+            assertEquals(STATUS_SUCCESS, it.status)
+            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+        }
+
+        agent.sendRemoveAllDscpPolicies()
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(STATUS_DELETED, it.status)
+            checkDscpValue(agent, callback, dstPort = 1111)
+        }
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(2, it.policyId)
+            assertEquals(STATUS_DELETED, it.status)
+            checkDscpValue(agent, callback, dstPort = 2222)
+        }
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(3, it.policyId)
+            assertEquals(STATUS_DELETED, it.status)
+            checkDscpValue(agent, callback, dstPort = 3333)
+        }
+    }
+
+    @Test
+    fun testAddDuplicateDscpPolicy(): Unit = createConnectedNetworkAgent().let {
+                (agent, callback) ->
+        val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(4444, 4444)).build()
+        agent.sendAddDscpPolicy(policy)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(STATUS_SUCCESS, it.status)
+            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 4444)
+        }
+
+        // TODO: send packet on socket and confirm that changing the DSCP policy
+        // updates the mark to the new value.
+
+        val policy2 = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(5555, 5555)).build()
+        agent.sendAddDscpPolicy(policy2)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(STATUS_SUCCESS, it.status)
+
+            // Sending packet with old policy should fail
+            checkDscpValue(agent, callback, dstPort = 4444)
+            checkDscpValue(agent, callback, dscpValue = 1, dstPort = 5555)
+        }
+
+        agent.sendRemoveDscpPolicy(1)
+        agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+            assertEquals(1, it.policyId)
+            assertEquals(STATUS_DELETED, it.status)
+        }
+    }
+
+    @Test
+    fun testParcelingDscpPolicyIsLossless(): Unit = createConnectedNetworkAgent().let {
+                (agent, callback) ->
+        // Check that policy with partial parameters is lossless.
+        val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(4444, 4444)).build()
+        assertParcelingIsLossless(policy);
+
+        // Check that policy with all parameters is lossless.
+        val policy2 = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(4444, 4444))
+                .setSourceAddress(LOCAL_IPV4_ADDRESS)
+                .setDestinationAddress(TEST_TARGET_IPV4_ADDR)
+                .setProtocol(IPPROTO_UDP).build()
+        assertParcelingIsLossless(policy2);
+    }
+}
+
+private fun ByteBuffer.readAsArray(): ByteArray {
+    val out = ByteArray(remaining())
+    get(out)
+    return out
+}
+
+private fun <T> Context.assertHasService(manager: Class<T>): T {
+    return getSystemService(manager) ?: fail("Service $manager not found")
+}
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/LocalSocketTest.java b/tests/cts/net/src/android/net/cts/LocalSocketTest.java
deleted file mode 100644
index 6e61705..0000000
--- a/tests/cts/net/src/android/net/cts/LocalSocketTest.java
+++ /dev/null
@@ -1,470 +0,0 @@
-/*
- * Copyright (C) 2008 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.cts;
-
-import junit.framework.TestCase;
-
-import android.net.Credentials;
-import android.net.LocalServerSocket;
-import android.net.LocalSocket;
-import android.net.LocalSocketAddress;
-import android.system.Os;
-import android.system.OsConstants;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.concurrent.Callable;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-
-public class LocalSocketTest extends TestCase {
-    private final static String ADDRESS_PREFIX = "com.android.net.LocalSocketTest";
-
-    public void testLocalConnections() throws IOException {
-        String address = ADDRESS_PREFIX + "_testLocalConnections";
-        // create client and server socket
-        LocalServerSocket localServerSocket = new LocalServerSocket(address);
-        LocalSocket clientSocket = new LocalSocket();
-
-        // establish connection between client and server
-        LocalSocketAddress locSockAddr = new LocalSocketAddress(address);
-        assertFalse(clientSocket.isConnected());
-        clientSocket.connect(locSockAddr);
-        assertTrue(clientSocket.isConnected());
-
-        LocalSocket serverSocket = localServerSocket.accept();
-        assertTrue(serverSocket.isConnected());
-        assertTrue(serverSocket.isBound());
-        try {
-            serverSocket.bind(localServerSocket.getLocalSocketAddress());
-            fail("Cannot bind a LocalSocket from accept()");
-        } catch (IOException expected) {
-        }
-        try {
-            serverSocket.connect(locSockAddr);
-            fail("Cannot connect a LocalSocket from accept()");
-        } catch (IOException expected) {
-        }
-
-        Credentials credent = clientSocket.getPeerCredentials();
-        assertTrue(0 != credent.getPid());
-
-        // send data from client to server
-        OutputStream clientOutStream = clientSocket.getOutputStream();
-        clientOutStream.write(12);
-        InputStream serverInStream = serverSocket.getInputStream();
-        assertEquals(12, serverInStream.read());
-
-        //send data from server to client
-        OutputStream serverOutStream = serverSocket.getOutputStream();
-        serverOutStream.write(3);
-        InputStream clientInStream = clientSocket.getInputStream();
-        assertEquals(3, clientInStream.read());
-
-        // Test sending and receiving file descriptors
-        clientSocket.setFileDescriptorsForSend(new FileDescriptor[]{FileDescriptor.in});
-        clientOutStream.write(32);
-        assertEquals(32, serverInStream.read());
-
-        FileDescriptor[] out = serverSocket.getAncillaryFileDescriptors();
-        assertEquals(1, out.length);
-        FileDescriptor fd = clientSocket.getFileDescriptor();
-        assertTrue(fd.valid());
-
-        //shutdown input stream of client
-        clientSocket.shutdownInput();
-        assertEquals(-1, clientInStream.read());
-
-        //shutdown output stream of client
-        clientSocket.shutdownOutput();
-        try {
-            clientOutStream.write(10);
-            fail("testLocalSocket shouldn't come to here");
-        } catch (IOException e) {
-            // expected
-        }
-
-        //shutdown input stream of server
-        serverSocket.shutdownInput();
-        assertEquals(-1, serverInStream.read());
-
-        //shutdown output stream of server
-        serverSocket.shutdownOutput();
-        try {
-            serverOutStream.write(10);
-            fail("testLocalSocket shouldn't come to here");
-        } catch (IOException e) {
-            // expected
-        }
-
-        //close client socket
-        clientSocket.close();
-        try {
-            clientInStream.read();
-            fail("testLocalSocket shouldn't come to here");
-        } catch (IOException e) {
-            // expected
-        }
-
-        //close server socket
-        serverSocket.close();
-        try {
-            serverInStream.read();
-            fail("testLocalSocket shouldn't come to here");
-        } catch (IOException e) {
-            // expected
-        }
-    }
-
-    public void testAccessors() throws IOException {
-        String address = ADDRESS_PREFIX + "_testAccessors";
-        LocalSocket socket = new LocalSocket();
-        LocalSocketAddress addr = new LocalSocketAddress(address);
-
-        assertFalse(socket.isBound());
-        socket.bind(addr);
-        assertTrue(socket.isBound());
-        assertEquals(addr, socket.getLocalSocketAddress());
-
-        String str = socket.toString();
-        assertTrue(str.contains("impl:android.net.LocalSocketImpl"));
-
-        socket.setReceiveBufferSize(1999);
-        assertEquals(1999 << 1, socket.getReceiveBufferSize());
-
-        socket.setSendBufferSize(3998);
-        assertEquals(3998 << 1, socket.getSendBufferSize());
-
-        assertEquals(0, socket.getSoTimeout());
-        socket.setSoTimeout(1996);
-        assertTrue(socket.getSoTimeout() > 0);
-
-        try {
-            socket.getRemoteSocketAddress();
-            fail("testLocalSocketSecondary shouldn't come to here");
-        } catch (UnsupportedOperationException e) {
-            // expected
-        }
-
-        try {
-            socket.isClosed();
-            fail("testLocalSocketSecondary shouldn't come to here");
-        } catch (UnsupportedOperationException e) {
-            // expected
-        }
-
-        try {
-            socket.isInputShutdown();
-            fail("testLocalSocketSecondary shouldn't come to here");
-        } catch (UnsupportedOperationException e) {
-            // expected
-        }
-
-        try {
-            socket.isOutputShutdown();
-            fail("testLocalSocketSecondary shouldn't come to here");
-        } catch (UnsupportedOperationException e) {
-            // expected
-        }
-
-        try {
-            socket.connect(addr, 2005);
-            fail("testLocalSocketSecondary shouldn't come to here");
-        } catch (UnsupportedOperationException e) {
-            // expected
-        }
-
-        socket.close();
-    }
-
-    // http://b/31205169
-    public void testSetSoTimeout_readTimeout() throws Exception {
-        String address = ADDRESS_PREFIX + "_testSetSoTimeout_readTimeout";
-
-        try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) {
-            final LocalSocket clientSocket = socketPair.clientSocket;
-
-            // Set the timeout in millis.
-            int timeoutMillis = 1000;
-            clientSocket.setSoTimeout(timeoutMillis);
-
-            // Avoid blocking the test run if timeout doesn't happen by using a separate thread.
-            Callable<Result> reader = () -> {
-                try {
-                    clientSocket.getInputStream().read();
-                    return Result.noException("Did not block");
-                } catch (IOException e) {
-                    return Result.exception(e);
-                }
-            };
-            // Allow the configured timeout, plus some slop.
-            int allowedTime = timeoutMillis + 2000;
-            Result result = runInSeparateThread(allowedTime, reader);
-
-            // Check the message was a timeout, it's all we have to go on.
-            String expectedMessage = Os.strerror(OsConstants.EAGAIN);
-            result.assertThrewIOException(expectedMessage);
-        }
-    }
-
-    // http://b/31205169
-    public void testSetSoTimeout_writeTimeout() throws Exception {
-        String address = ADDRESS_PREFIX + "_testSetSoTimeout_writeTimeout";
-
-        try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) {
-            final LocalSocket clientSocket = socketPair.clientSocket;
-
-            // Set the timeout in millis.
-            int timeoutMillis = 1000;
-            clientSocket.setSoTimeout(timeoutMillis);
-
-            // Set a small buffer size so we know we can flood it.
-            clientSocket.setSendBufferSize(100);
-            final int bufferSize = clientSocket.getSendBufferSize();
-
-            // Avoid blocking the test run if timeout doesn't happen by using a separate thread.
-            Callable<Result> writer = () -> {
-                try {
-                    byte[] toWrite = new byte[bufferSize * 2];
-                    clientSocket.getOutputStream().write(toWrite);
-                    return Result.noException("Did not block");
-                } catch (IOException e) {
-                    return Result.exception(e);
-                }
-            };
-            // Allow the configured timeout, plus some slop.
-            int allowedTime = timeoutMillis + 2000;
-
-            Result result = runInSeparateThread(allowedTime, writer);
-
-            // Check the message was a timeout, it's all we have to go on.
-            String expectedMessage = Os.strerror(OsConstants.EAGAIN);
-            result.assertThrewIOException(expectedMessage);
-        }
-    }
-
-    public void testAvailable() throws Exception {
-        String address = ADDRESS_PREFIX + "_testAvailable";
-
-        try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) {
-            LocalSocket clientSocket = socketPair.clientSocket;
-            LocalSocket serverSocket = socketPair.serverSocket.accept();
-
-            OutputStream clientOutputStream = clientSocket.getOutputStream();
-            InputStream serverInputStream = serverSocket.getInputStream();
-            assertEquals(0, serverInputStream.available());
-
-            byte[] buffer = new byte[50];
-            clientOutputStream.write(buffer);
-            assertEquals(50, serverInputStream.available());
-
-            InputStream clientInputStream = clientSocket.getInputStream();
-            OutputStream serverOutputStream = serverSocket.getOutputStream();
-            assertEquals(0, clientInputStream.available());
-            serverOutputStream.write(buffer);
-            assertEquals(50, serverInputStream.available());
-
-            serverSocket.close();
-        }
-    }
-
-    // http://b/34095140
-    public void testLocalSocketCreatedFromFileDescriptor() throws Exception {
-        String address = ADDRESS_PREFIX + "_testLocalSocketCreatedFromFileDescriptor";
-
-        // Establish connection between a local client and server to get a valid client socket file
-        // descriptor.
-        try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) {
-            // Extract the client FileDescriptor we can use.
-            FileDescriptor fileDescriptor = socketPair.clientSocket.getFileDescriptor();
-            assertTrue(fileDescriptor.valid());
-
-            // Create the LocalSocket we want to test.
-            LocalSocket clientSocketCreatedFromFileDescriptor =
-                    LocalSocket.createConnectedLocalSocket(fileDescriptor);
-            assertTrue(clientSocketCreatedFromFileDescriptor.isConnected());
-            assertTrue(clientSocketCreatedFromFileDescriptor.isBound());
-
-            // Test the LocalSocket can be used for communication.
-            LocalSocket serverSocket = socketPair.serverSocket.accept();
-            OutputStream clientOutputStream =
-                    clientSocketCreatedFromFileDescriptor.getOutputStream();
-            InputStream serverInputStream = serverSocket.getInputStream();
-
-            clientOutputStream.write(12);
-            assertEquals(12, serverInputStream.read());
-
-            // Closing clientSocketCreatedFromFileDescriptor does not close the file descriptor.
-            clientSocketCreatedFromFileDescriptor.close();
-            assertTrue(fileDescriptor.valid());
-
-            // .. while closing the LocalSocket that owned the file descriptor does.
-            socketPair.clientSocket.close();
-            assertFalse(fileDescriptor.valid());
-        }
-    }
-
-    public void testFlush() throws Exception {
-        String address = ADDRESS_PREFIX + "_testFlush";
-
-        try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) {
-            LocalSocket clientSocket = socketPair.clientSocket;
-            LocalSocket serverSocket = socketPair.serverSocket.accept();
-
-            OutputStream clientOutputStream = clientSocket.getOutputStream();
-            InputStream serverInputStream = serverSocket.getInputStream();
-            testFlushWorks(clientOutputStream, serverInputStream);
-
-            OutputStream serverOutputStream = serverSocket.getOutputStream();
-            InputStream clientInputStream = clientSocket.getInputStream();
-            testFlushWorks(serverOutputStream, clientInputStream);
-
-            serverSocket.close();
-        }
-    }
-
-    private void testFlushWorks(OutputStream outputStream, InputStream inputStream)
-            throws Exception {
-        final int bytesToTransfer = 50;
-        StreamReader inputStreamReader = new StreamReader(inputStream, bytesToTransfer);
-
-        byte[] buffer = new byte[bytesToTransfer];
-        outputStream.write(buffer);
-        assertEquals(bytesToTransfer, inputStream.available());
-
-        // Start consuming the data.
-        inputStreamReader.start();
-
-        // This doesn't actually flush any buffers, it just polls until the reader has read all the
-        // bytes.
-        outputStream.flush();
-
-        inputStreamReader.waitForCompletion(5000);
-        inputStreamReader.assertBytesRead(bytesToTransfer);
-        assertEquals(0, inputStream.available());
-    }
-
-    private static class StreamReader extends Thread {
-        private final InputStream is;
-        private final int expectedByteCount;
-        private final CountDownLatch completeLatch = new CountDownLatch(1);
-
-        private volatile Exception exception;
-        private int bytesRead;
-
-        private StreamReader(InputStream is, int expectedByteCount) {
-            this.is = is;
-            this.expectedByteCount = expectedByteCount;
-        }
-
-        @Override
-        public void run() {
-            try {
-                byte[] buffer = new byte[10];
-                int readCount;
-                while ((readCount = is.read(buffer)) >= 0) {
-                    bytesRead += readCount;
-                    if (bytesRead >= expectedByteCount) {
-                        break;
-                    }
-                }
-            } catch (IOException e) {
-                exception = e;
-            } finally {
-                completeLatch.countDown();
-            }
-        }
-
-        public void waitForCompletion(long waitMillis) throws Exception {
-            if (!completeLatch.await(waitMillis, TimeUnit.MILLISECONDS)) {
-                fail("Timeout waiting for completion");
-            }
-            if (exception != null) {
-                throw new Exception("Read failed", exception);
-            }
-        }
-
-        public void assertBytesRead(int expected) {
-            assertEquals(expected, bytesRead);
-        }
-    }
-
-    private static class Result {
-        private final String type;
-        private final Exception e;
-
-        private Result(String type, Exception e) {
-            this.type = type;
-            this.e = e;
-        }
-
-        static Result noException(String description) {
-            return new Result(description, null);
-        }
-
-        static Result exception(Exception e) {
-            return new Result(e.getClass().getName(), e);
-        }
-
-        void assertThrewIOException(String expectedMessage) {
-            assertEquals("Unexpected result type", IOException.class.getName(), type);
-            assertEquals("Unexpected exception message", expectedMessage, e.getMessage());
-        }
-    }
-
-    private static Result runInSeparateThread(int allowedTime, final Callable<Result> callable)
-            throws Exception {
-        ExecutorService service = Executors.newSingleThreadScheduledExecutor();
-        Future<Result> future = service.submit(callable);
-        Result result = future.get(allowedTime, TimeUnit.MILLISECONDS);
-        if (!future.isDone()) {
-            fail("Worker thread appears blocked");
-        }
-        return result;
-    }
-
-    private static class LocalSocketPair implements AutoCloseable {
-        static LocalSocketPair createConnectedSocketPair(String address) throws Exception {
-            LocalServerSocket localServerSocket = new LocalServerSocket(address);
-            final LocalSocket clientSocket = new LocalSocket();
-
-            // Establish connection between client and server
-            LocalSocketAddress locSockAddr = new LocalSocketAddress(address);
-            clientSocket.connect(locSockAddr);
-            assertTrue(clientSocket.isConnected());
-            return new LocalSocketPair(localServerSocket, clientSocket);
-        }
-
-        final LocalServerSocket serverSocket;
-        final LocalSocket clientSocket;
-
-        LocalSocketPair(LocalServerSocket serverSocket, LocalSocket clientSocket) {
-            this.serverSocket = serverSocket;
-            this.clientSocket = clientSocket;
-        }
-
-        public void close() throws Exception {
-            serverSocket.close();
-            clientSocket.close();
-        }
-    }
-}
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index 7b5b44f..4d4e7b9 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -53,6 +53,7 @@
         // android_library does not include JNI libs: include NetworkStack dependencies here
         "libnativehelper_compat_libc++",
         "libnetworkstackutilsjni",
+        "libcom_android_connectivity_com_android_net_module_util_jni",
     ],
     jarjar_rules: ":connectivity-jarjar-rules",
 }
diff --git a/tests/mts/Android.bp b/tests/mts/Android.bp
index a56f76e..2c44010 100644
--- a/tests/mts/Android.bp
+++ b/tests/mts/Android.bp
@@ -23,6 +23,9 @@
         "general-tests",
         "mts-tethering",
     ],
+    defaults: [
+        "connectivity-mainline-presubmit-cc-defaults",
+    ],
     require_root: true,
     static_libs: [
         "libbase",
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 48751f4..25b391a 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -23,6 +23,7 @@
     name: "FrameworksNetTests-jni-defaults",
     jni_libs: [
         "ld-android",
+        "libandroid_net_frameworktests_util_jni",
         "libbase",
         "libbinder",
         "libbpf_bcc",
@@ -38,8 +39,8 @@
         "liblog",
         "liblzma",
         "libnativehelper",
-        "libnetdbpf",
         "libnetdutils",
+        "libnetworkstats",
         "libnetworkstatsfactorytestjni",
         "libpackagelistparser",
         "libpcre2",
@@ -74,6 +75,7 @@
         "java/android/net/TelephonyNetworkSpecifierTest.java",
         "java/android/net/VpnManagerTest.java",
         "java/android/net/ipmemorystore/*.java",
+        "java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt",
         "java/android/net/nsd/*.java",
         "java/com/android/internal/net/NetworkUtilsInternalTest.java",
         "java/com/android/internal/net/VpnProfileTest.java",
@@ -88,7 +90,9 @@
         "java/com/android/server/connectivity/NetdEventListenerServiceTest.java",
         "java/com/android/server/connectivity/VpnTest.java",
         "java/com/android/server/net/ipmemorystore/*.java",
+        "java/com/android/server/net/BpfInterfaceMapUpdaterTest.java",
         "java/com/android/server/net/NetworkStats*.java",
+        "java/com/android/server/net/TestableUsageCallback.kt",
     ]
 }
 
@@ -141,6 +145,7 @@
         "ServiceConnectivityResources",
     ],
     visibility: ["//packages/modules/Connectivity/tests:__subpackages__"],
+    exclude_kotlinc_generated_files: false,
 }
 
 android_test {
diff --git a/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
index 6e51069..561e621 100644
--- a/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
+++ b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
@@ -240,6 +240,27 @@
         assertFalse(stats.hasNextBucket());
     }
 
+
+    @Test
+    public void testQueryDetailsForDevice() throws Exception {
+        final long startTime = 1;
+        final long endTime = 100;
+
+        reset(mStatsSession);
+        when(mService.openSessionForUsageStats(anyInt(), anyString())).thenReturn(mStatsSession);
+        when(mStatsSession.getHistoryIntervalForNetwork(any(NetworkTemplate.class),
+                anyInt(), anyLong(), anyLong()))
+                .thenReturn(new NetworkStatsHistory(10, 0));
+        final NetworkTemplate template = new NetworkTemplate.Builder(NetworkTemplate.MATCH_MOBILE)
+                .setMeteredness(NetworkStats.Bucket.METERED_YES).build();
+        NetworkStats stats = mManager.queryDetailsForDevice(template, startTime, endTime);
+
+        verify(mStatsSession, times(1)).getHistoryIntervalForNetwork(
+                eq(template), eq(NetworkStatsHistory.FIELD_ALL), eq(startTime), eq(endTime));
+
+        assertFalse(stats.hasNextBucket());
+    }
+
     private void assertBucketMatches(Entry expected, NetworkStats.Bucket actual) {
         assertEquals(expected.uid, actual.getUid());
         assertEquals(expected.rxBytes, actual.getRxBytes());
diff --git a/tests/unit/java/android/net/NetworkIdentityTest.kt b/tests/unit/java/android/net/NetworkIdentityTest.kt
index b1ffc92..4b2d874 100644
--- a/tests/unit/java/android/net/NetworkIdentityTest.kt
+++ b/tests/unit/java/android/net/NetworkIdentityTest.kt
@@ -17,11 +17,16 @@
 package android.net
 
 import android.content.Context
+import android.net.ConnectivityManager.MAX_NETWORK_TYPE
+import android.net.ConnectivityManager.TYPE_ETHERNET
 import android.net.ConnectivityManager.TYPE_MOBILE
+import android.net.ConnectivityManager.TYPE_NONE
+import android.net.ConnectivityManager.TYPE_WIFI
 import android.net.NetworkIdentity.OEM_NONE
 import android.net.NetworkIdentity.OEM_PAID
 import android.net.NetworkIdentity.OEM_PRIVATE
 import android.net.NetworkIdentity.getOemBitfield
+import android.app.usage.NetworkStatsManager
 import android.telephony.TelephonyManager
 import android.os.Build
 import com.android.testutils.DevSdkIgnoreRule
@@ -30,10 +35,12 @@
 import org.junit.runner.RunWith
 import org.mockito.Mockito.mock
 import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
 
 private const val TEST_IMSI = "testimsi"
+private const val TEST_WIFI_KEY = "testwifikey"
 
 @RunWith(DevSdkIgnoreRunner::class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
@@ -74,12 +81,12 @@
     }
 
     @Test
-    fun testGetMetered() {
+    fun testIsMetered() {
         // Verify network is metered.
         val netIdent1 = NetworkIdentity.buildNetworkIdentity(mockContext,
                 buildMobileNetworkStateSnapshot(NetworkCapabilities(), TEST_IMSI),
                 false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
-        assertTrue(netIdent1.getMetered())
+        assertTrue(netIdent1.isMetered())
 
         // Verify network is not metered because it has NET_CAPABILITY_NOT_METERED capability.
         val capsNotMetered = NetworkCapabilities.Builder().apply {
@@ -88,7 +95,7 @@
         val netIdent2 = NetworkIdentity.buildNetworkIdentity(mockContext,
                 buildMobileNetworkStateSnapshot(capsNotMetered, TEST_IMSI),
                 false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
-        assertFalse(netIdent2.getMetered())
+        assertFalse(netIdent2.isMetered())
 
         // Verify network is not metered because it has NET_CAPABILITY_TEMPORARILY_NOT_METERED
         // capability .
@@ -98,6 +105,121 @@
         val netIdent3 = NetworkIdentity.buildNetworkIdentity(mockContext,
                 buildMobileNetworkStateSnapshot(capsTempNotMetered, TEST_IMSI),
                 false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
-        assertFalse(netIdent3.getMetered())
+        assertFalse(netIdent3.isMetered())
+    }
+
+    @Test
+    fun testBuilder() {
+        val oemPrivateRoamingNotMeteredCap = NetworkCapabilities().apply {
+            addCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE)
+            addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+        }
+        val identFromSnapshot = NetworkIdentity.Builder().setNetworkStateSnapshot(
+                buildMobileNetworkStateSnapshot(oemPrivateRoamingNotMeteredCap, TEST_IMSI))
+                .setDefaultNetwork(true)
+                .setRatType(TelephonyManager.NETWORK_TYPE_UMTS)
+                .build()
+        val identFromLegacyBuild = NetworkIdentity.buildNetworkIdentity(mockContext,
+                buildMobileNetworkStateSnapshot(oemPrivateRoamingNotMeteredCap, TEST_IMSI),
+                true /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
+        val identFromConstructor = NetworkIdentity(TYPE_MOBILE,
+                TelephonyManager.NETWORK_TYPE_UMTS,
+                TEST_IMSI,
+                null /* wifiNetworkKey */,
+                true /* roaming */,
+                false /* metered */,
+                true /* defaultNetwork */,
+                NetworkTemplate.OEM_MANAGED_PRIVATE)
+        assertEquals(identFromLegacyBuild, identFromSnapshot)
+        assertEquals(identFromConstructor, identFromSnapshot)
+
+        // Assert non-wifi can't have wifi network key.
+        assertFailsWith<IllegalArgumentException> {
+            NetworkIdentity.Builder()
+                    .setType(TYPE_ETHERNET)
+                    .setWifiNetworkKey(TEST_WIFI_KEY)
+                    .build()
+        }
+
+        // Assert non-mobile can't have ratType.
+        assertFailsWith<IllegalArgumentException> {
+            NetworkIdentity.Builder()
+                    .setType(TYPE_WIFI)
+                    .setRatType(TelephonyManager.NETWORK_TYPE_LTE)
+                    .build()
+        }
+    }
+
+    @Test
+    fun testBuilder_type() {
+        // Assert illegal type values cannot make an identity.
+        listOf(Integer.MIN_VALUE, TYPE_NONE - 1, MAX_NETWORK_TYPE + 1, Integer.MAX_VALUE)
+                .forEach { type ->
+                    assertFailsWith<IllegalArgumentException> {
+                        NetworkIdentity.Builder().setType(type).build()
+                    }
+                }
+
+        // Verify legitimate type values can make an identity.
+        for (type in TYPE_NONE..MAX_NETWORK_TYPE) {
+            NetworkIdentity.Builder().setType(type).build().also {
+                assertEquals(it.type, type)
+            }
+        }
+    }
+
+    @Test
+    fun testBuilder_ratType() {
+        // Assert illegal ratTypes cannot make an identity.
+        listOf(Integer.MIN_VALUE, NetworkTemplate.NETWORK_TYPE_ALL,
+                NetworkStatsManager.NETWORK_TYPE_5G_NSA - 1, Integer.MAX_VALUE)
+                .forEach {
+                    assertFailsWith<IllegalArgumentException> {
+                        NetworkIdentity.Builder()
+                                .setType(TYPE_MOBILE)
+                                .setRatType(it)
+                                .build()
+                    }
+                }
+
+        // Verify legitimate ratTypes can make an identity.
+        TelephonyManager.getAllNetworkTypes().toMutableList().also {
+            it.add(TelephonyManager.NETWORK_TYPE_UNKNOWN)
+            it.add(NetworkStatsManager.NETWORK_TYPE_5G_NSA)
+        }.forEach { rat ->
+            NetworkIdentity.Builder()
+                    .setType(TYPE_MOBILE)
+                    .setRatType(rat)
+                    .build().also {
+                        assertEquals(it.ratType, rat)
+                    }
+        }
+    }
+
+    @Test
+    fun testBuilder_oemManaged() {
+        // Assert illegal oemManage values cannot make an identity.
+        listOf(Integer.MIN_VALUE, NetworkTemplate.OEM_MANAGED_ALL, NetworkTemplate.OEM_MANAGED_YES,
+                Integer.MAX_VALUE)
+                .forEach { oemManaged ->
+                    assertFailsWith<IllegalArgumentException> {
+                        NetworkIdentity.Builder()
+                                .setType(TYPE_MOBILE)
+                                .setOemManaged(oemManaged)
+                                .build()
+                    }
+                }
+
+        // Verify legitimate oem managed values can make an identity.
+        listOf(NetworkTemplate.OEM_MANAGED_NO, NetworkTemplate.OEM_MANAGED_PAID,
+                NetworkTemplate.OEM_MANAGED_PRIVATE, NetworkTemplate.OEM_MANAGED_PAID or
+                NetworkTemplate.OEM_MANAGED_PRIVATE)
+                .forEach { oemManaged ->
+                    NetworkIdentity.Builder()
+                            .setOemManaged(oemManaged)
+                            .build().also {
+                                assertEquals(it.oemManaged, oemManaged)
+                            }
+                }
     }
 }
diff --git a/tests/unit/java/android/net/NetworkStatsAccessTest.java b/tests/unit/java/android/net/NetworkStatsAccessTest.java
index e4fc118..97a93ca 100644
--- a/tests/unit/java/android/net/NetworkStatsAccessTest.java
+++ b/tests/unit/java/android/net/NetworkStatsAccessTest.java
@@ -16,6 +16,8 @@
 
 package android.net;
 
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
 import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.when;
 
@@ -25,7 +27,6 @@
 import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
-import android.os.Build;
 import android.telephony.TelephonyManager;
 
 import androidx.test.filters.SmallTest;
@@ -42,7 +43,7 @@
 
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
 public class NetworkStatsAccessTest {
     private static final String TEST_PKG = "com.example.test";
     private static final int TEST_PID = 1234;
diff --git a/tests/unit/java/android/net/NetworkStatsCollectionTest.java b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
index 1c557d6..c27ee93 100644
--- a/tests/unit/java/android/net/NetworkStatsCollectionTest.java
+++ b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
@@ -29,6 +29,7 @@
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 
 import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 import static com.android.testutils.MiscAsserts.assertThrows;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -37,12 +38,13 @@
 import static org.junit.Assert.fail;
 
 import android.content.res.Resources;
-import android.os.Build;
+import android.net.NetworkStatsCollection.Key;
 import android.os.Process;
 import android.os.UserHandle;
 import android.telephony.SubscriptionPlan;
 import android.telephony.TelephonyManager;
 import android.text.format.DateUtils;
+import android.util.ArrayMap;
 import android.util.RecurrenceRule;
 
 import androidx.test.InstrumentationRegistry;
@@ -59,6 +61,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mockito;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -72,13 +75,14 @@
 import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Tests for {@link NetworkStatsCollection}.
  */
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
 public class NetworkStatsCollectionTest {
 
     private static final String TEST_FILE = "test.bin";
@@ -195,8 +199,8 @@
         // record empty data straddling between buckets
         final NetworkStats.Entry entry = new NetworkStats.Entry();
         entry.rxBytes = 32;
-        collection.recordData(null, UID_ALL, SET_DEFAULT, TAG_NONE, 30 * MINUTE_IN_MILLIS,
-                90 * MINUTE_IN_MILLIS, entry);
+        collection.recordData(Mockito.mock(NetworkIdentitySet.class), UID_ALL, SET_DEFAULT,
+                TAG_NONE, 30 * MINUTE_IN_MILLIS, 90 * MINUTE_IN_MILLIS, entry);
 
         // assert that we report boundary in atomic buckets
         assertEquals(0, collection.getStartMillis());
@@ -529,6 +533,52 @@
         assertThrows(ArithmeticException.class, () -> multiplySafeByRational(30, 3, 0));
     }
 
+    @Test
+    public void testBuilder() {
+        final Map<Key, NetworkStatsHistory> expectedEntries = new ArrayMap<>();
+        final NetworkStats.Entry entry = new NetworkStats.Entry();
+        final NetworkIdentitySet ident = new NetworkIdentitySet();
+        final Key key1 = new Key(ident, 0, 0, 0);
+        final Key key2 = new Key(ident, 1, 0, 0);
+        final long bucketDuration = 10;
+
+        final NetworkStatsHistory.Entry entry1 = new NetworkStatsHistory.Entry(10, 10, 40,
+                4, 50, 5, 60);
+        final NetworkStatsHistory.Entry entry2 = new NetworkStatsHistory.Entry(30, 10, 3,
+                41, 7, 1, 0);
+
+        NetworkStatsHistory history1 = new NetworkStatsHistory.Builder(10, 5)
+                .addEntry(entry1)
+                .addEntry(entry2)
+                .build();
+
+        NetworkStatsHistory history2 = new NetworkStatsHistory(10, 5);
+
+        NetworkStatsCollection actualCollection = new NetworkStatsCollection.Builder(bucketDuration)
+                .addEntry(key1, history1)
+                .addEntry(key2, history2)
+                .build();
+
+        // The builder will omit any entry with empty history. Thus, history2
+        // is not expected in the result collection.
+        expectedEntries.put(key1, history1);
+
+        final Map<Key, NetworkStatsHistory> actualEntries = actualCollection.getEntries();
+
+        assertEquals(expectedEntries.size(), actualEntries.size());
+        for (Key expectedKey : expectedEntries.keySet()) {
+            final NetworkStatsHistory expectedHistory = expectedEntries.get(expectedKey);
+
+            final NetworkStatsHistory actualHistory = actualEntries.get(expectedKey);
+            assertNotNull(actualHistory);
+
+            assertEquals(expectedHistory.getEntries(), actualHistory.getEntries());
+
+            actualEntries.remove(expectedKey);
+        }
+        assertEquals(0, actualEntries.size());
+    }
+
     /**
      * Copy a {@link Resources#openRawResource(int)} into {@link File} for
      * testing purposes.
@@ -586,6 +636,14 @@
                 actual.txBytes, actual.txPackets, 0L));
     }
 
+    private static void assertEntry(NetworkStatsHistory.Entry expected,
+            NetworkStatsHistory.Entry actual) {
+        assertEntry(new NetworkStats.Entry(actual.rxBytes, actual.rxPackets,
+                actual.txBytes, actual.txPackets, 0L),
+                new NetworkStats.Entry(actual.rxBytes, actual.rxPackets,
+                actual.txBytes, actual.txPackets, 0L));
+    }
+
     private static void assertEntry(NetworkStats.Entry expected,
             NetworkStats.Entry actual) {
         assertEquals("unexpected rxBytes", expected.rxBytes, actual.rxBytes);
diff --git a/tests/unit/java/android/net/NetworkStatsHistoryTest.java b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
index c5f8c00..c170605 100644
--- a/tests/unit/java/android/net/NetworkStatsHistoryTest.java
+++ b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
@@ -56,6 +56,7 @@
 import java.io.ByteArrayOutputStream;
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
+import java.util.List;
 import java.util.Random;
 
 @RunWith(DevSdkIgnoreRunner.class)
@@ -532,6 +533,40 @@
         assertEquals(512L + 4096L, stats.getTotalBytes());
     }
 
+    @Test
+    public void testBuilder() {
+        final NetworkStatsHistory.Entry entry1 = new NetworkStatsHistory.Entry(10, 30, 40,
+                4, 50, 5, 60);
+        final NetworkStatsHistory.Entry entry2 = new NetworkStatsHistory.Entry(30, 15, 3,
+                41, 7, 1, 0);
+        final NetworkStatsHistory.Entry entry3 = new NetworkStatsHistory.Entry(7, 301, 11,
+                14, 31, 2, 80);
+
+        final NetworkStatsHistory statsEmpty = new NetworkStatsHistory
+                .Builder(HOUR_IN_MILLIS, 10).build();
+        assertEquals(0, statsEmpty.getEntries().size());
+        assertEquals(HOUR_IN_MILLIS, statsEmpty.getBucketDuration());
+
+        NetworkStatsHistory statsSingle = new NetworkStatsHistory
+                .Builder(HOUR_IN_MILLIS, 8)
+                .addEntry(entry1)
+                .build();
+        assertEquals(1, statsSingle.getEntries().size());
+        assertEquals(HOUR_IN_MILLIS, statsSingle.getBucketDuration());
+        assertEquals(entry1, statsSingle.getEntries().get(0));
+
+        NetworkStatsHistory statsMultiple = new NetworkStatsHistory
+                .Builder(SECOND_IN_MILLIS, 0)
+                .addEntry(entry1).addEntry(entry2).addEntry(entry3)
+                .build();
+        final List<NetworkStatsHistory.Entry> entries = statsMultiple.getEntries();
+        assertEquals(3, entries.size());
+        assertEquals(SECOND_IN_MILLIS, statsMultiple.getBucketDuration());
+        assertEquals(entry1, entries.get(0));
+        assertEquals(entry2, entries.get(1));
+        assertEquals(entry3, entries.get(2));
+    }
+
     private static void assertIndexBeforeAfter(
             NetworkStatsHistory stats, int before, int after, long time) {
         assertEquals("unexpected before", before, stats.getIndexBefore(time));
diff --git a/tests/unit/java/android/net/NetworkTemplateTest.kt b/tests/unit/java/android/net/NetworkTemplateTest.kt
index 15db45c..453612f 100644
--- a/tests/unit/java/android/net/NetworkTemplateTest.kt
+++ b/tests/unit/java/android/net/NetworkTemplateTest.kt
@@ -16,13 +16,13 @@
 
 package android.net
 
+import android.app.usage.NetworkStatsManager.NETWORK_TYPE_5G_NSA
 import android.content.Context
 import android.net.ConnectivityManager.TYPE_MOBILE
 import android.net.ConnectivityManager.TYPE_WIFI
 import android.net.NetworkIdentity.OEM_NONE
 import android.net.NetworkIdentity.OEM_PAID
 import android.net.NetworkIdentity.OEM_PRIVATE
-import android.net.NetworkIdentity.SUBTYPE_COMBINED
 import android.net.NetworkIdentity.buildNetworkIdentity
 import android.net.NetworkStats.DEFAULT_NETWORK_ALL
 import android.net.NetworkStats.METERED_ALL
@@ -37,7 +37,6 @@
 import android.net.NetworkTemplate.MATCH_PROXY
 import android.net.NetworkTemplate.MATCH_WIFI
 import android.net.NetworkTemplate.MATCH_WIFI_WILDCARD
-import android.net.NetworkTemplate.NETWORK_TYPE_5G_NSA
 import android.net.NetworkTemplate.NETWORK_TYPE_ALL
 import android.net.NetworkTemplate.OEM_MANAGED_ALL
 import android.net.NetworkTemplate.OEM_MANAGED_NO
@@ -57,6 +56,7 @@
 import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.SC_V2
 import com.android.testutils.assertParcelSane
 import org.junit.Before
 import org.junit.Test
@@ -95,7 +95,7 @@
         oemManaged: Int = OEM_NONE,
         metered: Boolean = true
     ): NetworkStateSnapshot {
-        `when`(mockWifiInfo.getCurrentNetworkKey()).thenReturn(wifiKey)
+        `when`(mockWifiInfo.getNetworkKey()).thenReturn(wifiKey)
         val lp = LinkProperties()
         val caps = NetworkCapabilities().apply {
             setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, !metered)
@@ -312,7 +312,7 @@
         val identLteMetered = buildNetworkIdentity(
                 mockContext, stateMobileImsi1Metered, false, TelephonyManager.NETWORK_TYPE_LTE)
         val identCombinedMetered = buildNetworkIdentity(
-                mockContext, stateMobileImsi1Metered, false, SUBTYPE_COMBINED)
+                mockContext, stateMobileImsi1Metered, false, NetworkTemplate.NETWORK_TYPE_ALL)
         val identImsi2UmtsMetered = buildNetworkIdentity(mockContext,
                 buildMobileNetworkState(TEST_IMSI2), false, TelephonyManager.NETWORK_TYPE_UMTS)
         val identWifi = buildNetworkIdentity(
@@ -326,7 +326,7 @@
         val identLteNonMetered = buildNetworkIdentity(
                 mockContext, stateMobileImsi1NonMetered, false, TelephonyManager.NETWORK_TYPE_LTE)
         val identCombinedNonMetered = buildNetworkIdentity(
-                mockContext, stateMobileImsi1NonMetered, false, SUBTYPE_COMBINED)
+                mockContext, stateMobileImsi1NonMetered, false, NetworkTemplate.NETWORK_TYPE_ALL)
         val identImsi2UmtsNonMetered = buildNetworkIdentity(mockContext,
                 stateMobileImsi2NonMetered, false, TelephonyManager.NETWORK_TYPE_UMTS)
 
@@ -556,7 +556,7 @@
         }
     }
 
-    @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S)
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
     @Test
     fun testBuilderMatchRules() {
         // Verify unknown match rules cannot construct templates.
@@ -657,7 +657,7 @@
         }
     }
 
-    @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S)
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
     @Test
     fun testBuilderWifiNetworkKeys() {
         // Verify template builder which generates same template with the given different
diff --git a/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt b/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt
new file mode 100644
index 0000000..e4943ea
--- /dev/null
+++ b/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2022 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.netstats
+
+import android.net.NetworkStatsCollection
+import androidx.test.InstrumentationRegistry
+import androidx.test.filters.SmallTest
+import com.android.frameworks.tests.net.R
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.SC_V2
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+import java.io.DataInputStream
+import java.net.ProtocolException
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.fail
+
+private const val BUCKET_DURATION_MS = 2 * 60 * 60 * 1000L
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+class NetworkStatsDataMigrationUtilsTest {
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun testReadPlatformCollection() {
+        // Verify the method throws for wrong file version.
+        assertFailsWith<ProtocolException> {
+            NetworkStatsDataMigrationUtils.readPlatformCollection(
+                    NetworkStatsCollection.Builder(BUCKET_DURATION_MS),
+                    getInputStreamForResource(R.raw.netstats_uid_v4))
+        }
+
+        val builder = NetworkStatsCollection.Builder(BUCKET_DURATION_MS)
+        NetworkStatsDataMigrationUtils.readPlatformCollection(builder,
+                getInputStreamForResource(R.raw.netstats_uid_v16))
+        // The values are obtained by dumping from NetworkStatsCollection that
+        // read by the logic inside the service.
+        assertValues(builder.build(), 55, 1814302L, 21050L, 31001636L, 26152L)
+    }
+
+    @Test
+    fun testMaybeReadLegacyUid() {
+        val builder = NetworkStatsCollection.Builder(BUCKET_DURATION_MS)
+        NetworkStatsDataMigrationUtils.readLegacyUid(builder,
+                getInputStreamForResource(R.raw.netstats_uid_v4), false /* taggedData */)
+        assertValues(builder.build(), 223, 106245210L, 710722L, 1130647496L, 1103989L)
+    }
+
+    private fun assertValues(
+        collection: NetworkStatsCollection,
+        expectedSize: Int,
+        expectedTxBytes: Long,
+        expectedTxPackets: Long,
+        expectedRxBytes: Long,
+        expectedRxPackets: Long
+    ) {
+        var txBytes = 0L
+        var txPackets = 0L
+        var rxBytes = 0L
+        var rxPackets = 0L
+        val entries = collection.entries
+
+        for (history in entries.values) {
+            for (historyEntry in history.entries) {
+                txBytes += historyEntry.txBytes
+                txPackets += historyEntry.txPackets
+                rxBytes += historyEntry.rxBytes
+                rxPackets += historyEntry.rxPackets
+            }
+        }
+        if (expectedSize != entries.size ||
+                expectedTxBytes != txBytes ||
+                expectedTxPackets != txPackets ||
+                expectedRxBytes != rxBytes ||
+                expectedRxPackets != rxPackets) {
+            fail("expected size=$expectedSize" +
+                    "txb=$expectedTxBytes txp=$expectedTxPackets " +
+                    "rxb=$expectedRxBytes rxp=$expectedRxPackets bus was " +
+                    "size=${entries.size} txb=$txBytes txp=$txPackets " +
+                    "rxb=$rxBytes rxp=$rxPackets")
+        }
+        assertEquals(txBytes + rxBytes, collection.totalBytes)
+    }
+
+    private fun getInputStreamForResource(resourceId: Int): DataInputStream {
+        return DataInputStream(InstrumentationRegistry.getContext()
+                .getResources().openRawResource(resourceId))
+    }
+}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index abb34dc..652aee9 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -101,6 +101,7 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_VSIM;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_XCAP;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
 import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION;
 import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
 import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
@@ -338,6 +339,7 @@
 import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
 import com.android.server.connectivity.ProxyTracker;
 import com.android.server.connectivity.QosCallbackTracker;
+import com.android.server.connectivity.UidRangeUtils;
 import com.android.server.connectivity.Vpn;
 import com.android.server.connectivity.VpnProfileStore;
 import com.android.server.net.NetworkPinner;
@@ -350,6 +352,7 @@
 import com.android.testutils.TestableNetworkOfferCallback;
 
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -443,6 +446,10 @@
     private static final int TEST_WORK_PROFILE_USER_ID = 2;
     private static final int TEST_WORK_PROFILE_APP_UID =
             UserHandle.getUid(TEST_WORK_PROFILE_USER_ID, TEST_APP_ID);
+    private static final int TEST_APP_ID_2 = 104;
+    private static final int TEST_WORK_PROFILE_APP_UID_2 =
+            UserHandle.getUid(TEST_WORK_PROFILE_USER_ID, TEST_APP_ID_2);
+
     private static final String CLAT_PREFIX = "v4-";
     private static final String MOBILE_IFNAME = "test_rmnet_data0";
     private static final String CLAT_MOBILE_IFNAME = CLAT_PREFIX + MOBILE_IFNAME;
@@ -492,6 +499,8 @@
     private TestNetworkCallback mSystemDefaultNetworkCallback;
     private TestNetworkCallback mProfileDefaultNetworkCallback;
     private TestNetworkCallback mTestPackageDefaultNetworkCallback;
+    private TestNetworkCallback mProfileDefaultNetworkCallbackAsAppUid2;
+    private TestNetworkCallback mTestPackageDefaultNetworkCallback2;
 
     // State variables required to emulate NetworkPolicyManagerService behaviour.
     private int mBlockedReasons = BLOCKED_REASON_NONE;
@@ -12247,6 +12256,8 @@
     private void registerDefaultNetworkCallbacks() {
         if (mSystemDefaultNetworkCallback != null || mDefaultNetworkCallback != null
                 || mProfileDefaultNetworkCallback != null
+                || mProfileDefaultNetworkCallbackAsAppUid2 != null
+                || mTestPackageDefaultNetworkCallback2 != null
                 || mTestPackageDefaultNetworkCallback != null) {
             throw new IllegalStateException("Default network callbacks already registered");
         }
@@ -12257,12 +12268,18 @@
         mDefaultNetworkCallback = new TestNetworkCallback();
         mProfileDefaultNetworkCallback = new TestNetworkCallback();
         mTestPackageDefaultNetworkCallback = new TestNetworkCallback();
+        mProfileDefaultNetworkCallbackAsAppUid2 = new TestNetworkCallback();
+        mTestPackageDefaultNetworkCallback2 = new TestNetworkCallback();
         mCm.registerSystemDefaultNetworkCallback(mSystemDefaultNetworkCallback,
                 new Handler(ConnectivityThread.getInstanceLooper()));
         mCm.registerDefaultNetworkCallback(mDefaultNetworkCallback);
         registerDefaultNetworkCallbackAsUid(mProfileDefaultNetworkCallback,
                 TEST_WORK_PROFILE_APP_UID);
         registerDefaultNetworkCallbackAsUid(mTestPackageDefaultNetworkCallback, TEST_PACKAGE_UID);
+        registerDefaultNetworkCallbackAsUid(mProfileDefaultNetworkCallbackAsAppUid2,
+                TEST_WORK_PROFILE_APP_UID_2);
+        registerDefaultNetworkCallbackAsUid(mTestPackageDefaultNetworkCallback2,
+                TEST_PACKAGE_UID2);
         // TODO: test using ConnectivityManager#registerDefaultNetworkCallbackAsUid as well.
         mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_DENIED);
     }
@@ -12280,6 +12297,12 @@
         if (null != mTestPackageDefaultNetworkCallback) {
             mCm.unregisterNetworkCallback(mTestPackageDefaultNetworkCallback);
         }
+        if (null != mProfileDefaultNetworkCallbackAsAppUid2) {
+            mCm.unregisterNetworkCallback(mProfileDefaultNetworkCallbackAsAppUid2);
+        }
+        if (null != mTestPackageDefaultNetworkCallback2) {
+            mCm.unregisterNetworkCallback(mTestPackageDefaultNetworkCallback2);
+        }
     }
 
     private void setupMultipleDefaultNetworksForOemNetworkPreferenceNotCurrentUidTest(
@@ -13708,10 +13731,34 @@
     }
 
     private UidRangeParcel[] uidRangeFor(final UserHandle handle) {
-        UidRange range = UidRange.createForUser(handle);
+        final UidRange range = UidRange.createForUser(handle);
         return new UidRangeParcel[] { new UidRangeParcel(range.start, range.stop) };
     }
 
+    private UidRangeParcel[] uidRangeFor(final UserHandle handle,
+            ProfileNetworkPreference profileNetworkPreference) {
+        final Set<UidRange> uidRangeSet;
+        UidRange range = UidRange.createForUser(handle);
+        if (profileNetworkPreference.getIncludedUids().size() != 0) {
+            uidRangeSet = UidRangeUtils.convertListToUidRange(
+                    profileNetworkPreference.getIncludedUids());
+        } else if (profileNetworkPreference.getExcludedUids().size() != 0)  {
+            uidRangeSet = UidRangeUtils.removeRangeSetFromUidRange(
+                    range, UidRangeUtils.convertListToUidRange(
+                            profileNetworkPreference.getExcludedUids()));
+        } else {
+            uidRangeSet = new ArraySet<>();
+            uidRangeSet.add(range);
+        }
+        UidRangeParcel[] uidRangeParcels = new UidRangeParcel[uidRangeSet.size()];
+        int i = 0;
+        for (UidRange range1 : uidRangeSet) {
+            uidRangeParcels[i] = new UidRangeParcel(range1.start, range1.stop);
+            i++;
+        }
+        return uidRangeParcels;
+    }
+
     private static class TestOnCompleteListener implements Runnable {
         final class OnComplete {}
         final ArrayTrackRecord<OnComplete>.ReadHead mHistory =
@@ -13734,6 +13781,14 @@
         return new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, new LinkProperties(), workNc);
     }
 
+    private TestNetworkAgentWrapper makeEnterpriseNetworkAgent(int enterpriseId) throws Exception {
+        final NetworkCapabilities workNc = new NetworkCapabilities();
+        workNc.addCapability(NET_CAPABILITY_ENTERPRISE);
+        workNc.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+        workNc.addEnterpriseId(enterpriseId);
+        return new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, new LinkProperties(), workNc);
+    }
+
     private TestNetworkCallback mEnterpriseCallback;
     private UserHandle setupEnterpriseNetwork() {
         final UserHandle userHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID);
@@ -13762,21 +13817,27 @@
      */
     public void testPreferenceForUserNetworkUpDownForGivenPreference(
             ProfileNetworkPreference profileNetworkPreference,
-            boolean connectWorkProfileAgentAhead) throws Exception {
+            boolean connectWorkProfileAgentAhead,
+            UserHandle testHandle,
+            TestNetworkCallback profileDefaultNetworkCallback,
+            TestNetworkCallback disAllowProfileDefaultNetworkCallback) throws Exception {
         final InOrder inOrder = inOrder(mMockNetd);
-        final UserHandle testHandle = setupEnterpriseNetwork();
-        registerDefaultNetworkCallbacks();
 
         mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
         mCellNetworkAgent.connect(true);
 
         mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
         mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
-        mProfileDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        profileDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        if (disAllowProfileDefaultNetworkCallback != null) {
+            disAllowProfileDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(
+                    mCellNetworkAgent);
+        }
         inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
                 mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE));
 
-        final TestNetworkAgentWrapper workAgent = makeEnterpriseNetworkAgent();
+        final TestNetworkAgentWrapper workAgent =
+                makeEnterpriseNetworkAgent(profileNetworkPreference.getPreferenceEnterpriseId());
         if (connectWorkProfileAgentAhead) {
             workAgent.connect(false);
         }
@@ -13790,7 +13851,7 @@
                 == PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK) {
             allowFallback = false;
         }
-        if (allowFallback) {
+        if (allowFallback && !connectWorkProfileAgentAhead) {
             // Setting a network preference for this user will create a new set of routing rules for
             // the UID range that corresponds to this user, inorder to define the default network
             // for these apps separately. This is true because the multi-layer request relevant to
@@ -13800,33 +13861,43 @@
             // system default is not handled specially, the rules are always active as long as
             // a preference is set.
             inOrder.verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
-                    mCellNetworkAgent.getNetwork().netId, uidRangeFor(testHandle),
+                    mCellNetworkAgent.getNetwork().netId,
+                    uidRangeFor(testHandle, profileNetworkPreference),
                     PREFERENCE_ORDER_PROFILE));
         }
 
         // The enterprise network is not ready yet.
         assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
-        if (allowFallback) {
-            assertNoCallbacks(mProfileDefaultNetworkCallback);
+        if (allowFallback && !connectWorkProfileAgentAhead) {
+            assertNoCallbacks(profileDefaultNetworkCallback);
         } else if (!connectWorkProfileAgentAhead) {
-            mProfileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+            profileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+            if (disAllowProfileDefaultNetworkCallback != null) {
+                assertNoCallbacks(disAllowProfileDefaultNetworkCallback);
+            }
         }
 
         if (!connectWorkProfileAgentAhead) {
             workAgent.connect(false);
         }
 
-        mProfileDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(workAgent);
+        profileDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(workAgent);
+        if (disAllowProfileDefaultNetworkCallback != null) {
+            disAllowProfileDefaultNetworkCallback.assertNoCallback();
+        }
         mSystemDefaultNetworkCallback.assertNoCallback();
         mDefaultNetworkCallback.assertNoCallback();
         inOrder.verify(mMockNetd).networkCreate(
                 nativeNetworkConfigPhysical(workAgent.getNetwork().netId, INetd.PERMISSION_SYSTEM));
         inOrder.verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
-                workAgent.getNetwork().netId, uidRangeFor(testHandle), PREFERENCE_ORDER_PROFILE));
+                workAgent.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreference),
+                PREFERENCE_ORDER_PROFILE));
 
-        if (allowFallback) {
+        if (allowFallback && !connectWorkProfileAgentAhead) {
             inOrder.verify(mMockNetd).networkRemoveUidRangesParcel(new NativeUidRangeConfig(
-                    mCellNetworkAgent.getNetwork().netId, uidRangeFor(testHandle),
+                    mCellNetworkAgent.getNetwork().netId,
+                    uidRangeFor(testHandle, profileNetworkPreference),
                     PREFERENCE_ORDER_PROFILE));
         }
 
@@ -13834,14 +13905,23 @@
         // not to the other apps.
         workAgent.setNetworkValid(true /* isStrictMode */);
         workAgent.mNetworkMonitor.forceReevaluation(Process.myUid());
-        mProfileDefaultNetworkCallback.expectCapabilitiesThat(workAgent,
+        profileDefaultNetworkCallback.expectCapabilitiesThat(workAgent,
                 nc -> nc.hasCapability(NET_CAPABILITY_VALIDATED)
-                        && nc.hasCapability(NET_CAPABILITY_ENTERPRISE));
+                        && nc.hasCapability(NET_CAPABILITY_ENTERPRISE)
+                        && nc.hasEnterpriseId(
+                                profileNetworkPreference.getPreferenceEnterpriseId())
+                        && nc.getEnterpriseIds().length == 1);
+        if (disAllowProfileDefaultNetworkCallback != null) {
+            assertNoCallbacks(disAllowProfileDefaultNetworkCallback);
+        }
         assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
 
         workAgent.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED);
-        mProfileDefaultNetworkCallback.expectCapabilitiesThat(workAgent, nc ->
+        profileDefaultNetworkCallback.expectCapabilitiesThat(workAgent, nc ->
                 nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
+        if (disAllowProfileDefaultNetworkCallback != null) {
+            assertNoCallbacks(disAllowProfileDefaultNetworkCallback);
+        }
         assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
 
         // Conversely, change a capability on the system-wide default network and make sure
@@ -13851,7 +13931,11 @@
                 nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
         mDefaultNetworkCallback.expectCapabilitiesThat(mCellNetworkAgent, nc ->
                 nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
-        mProfileDefaultNetworkCallback.assertNoCallback();
+        if (disAllowProfileDefaultNetworkCallback != null) {
+            disAllowProfileDefaultNetworkCallback.expectCapabilitiesThat(mCellNetworkAgent, nc ->
+                    nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
+        }
+        profileDefaultNetworkCallback.assertNoCallback();
 
         // Disconnect and reconnect the system-wide default network and make sure that the
         // apps on this network see the appropriate callbacks, and the app on the work profile
@@ -13859,28 +13943,41 @@
         mCellNetworkAgent.disconnect();
         mSystemDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
         mDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
-        mProfileDefaultNetworkCallback.assertNoCallback();
+        if (disAllowProfileDefaultNetworkCallback != null) {
+            disAllowProfileDefaultNetworkCallback.expectCallback(
+                    CallbackEntry.LOST, mCellNetworkAgent);
+        }
+        profileDefaultNetworkCallback.assertNoCallback();
         inOrder.verify(mMockNetd).networkDestroy(mCellNetworkAgent.getNetwork().netId);
 
         mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
         mCellNetworkAgent.connect(true);
         mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
         mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
-        mProfileDefaultNetworkCallback.assertNoCallback();
+        if (disAllowProfileDefaultNetworkCallback != null) {
+            disAllowProfileDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(
+                    mCellNetworkAgent);
+
+        }
+        profileDefaultNetworkCallback.assertNoCallback();
         inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
                 mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE));
 
         // When the agent disconnects, test that the app on the work profile falls back to the
         // default network.
         workAgent.disconnect();
-        mProfileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, workAgent);
+        profileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, workAgent);
         if (allowFallback) {
-            mProfileDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+            profileDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+            if (disAllowProfileDefaultNetworkCallback != null) {
+                assertNoCallbacks(disAllowProfileDefaultNetworkCallback);
+            }
         }
         assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
         if (allowFallback) {
             inOrder.verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
-                    mCellNetworkAgent.getNetwork().netId, uidRangeFor(testHandle),
+                    mCellNetworkAgent.getNetwork().netId,
+                    uidRangeFor(testHandle, profileNetworkPreference),
                     PREFERENCE_ORDER_PROFILE));
         }
         inOrder.verify(mMockNetd).networkDestroy(workAgent.getNetwork().netId);
@@ -13888,8 +13985,12 @@
         mCellNetworkAgent.disconnect();
         mSystemDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
         mDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        if (disAllowProfileDefaultNetworkCallback != null) {
+            disAllowProfileDefaultNetworkCallback.expectCallback(
+                    CallbackEntry.LOST, mCellNetworkAgent);
+        }
         if (allowFallback) {
-            mProfileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+            profileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
         }
 
         // Waiting for the handler to be idle before checking for networkDestroy is necessary
@@ -13900,33 +14001,47 @@
         // If the control comes here, callbacks seem to behave correctly in the presence of
         // a default network when the enterprise network goes up and down. Now, make sure they
         // also behave correctly in the absence of a system-wide default network.
-        final TestNetworkAgentWrapper workAgent2 = makeEnterpriseNetworkAgent();
+        final TestNetworkAgentWrapper workAgent2 =
+                makeEnterpriseNetworkAgent(profileNetworkPreference.getPreferenceEnterpriseId());
         workAgent2.connect(false);
 
-        mProfileDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(workAgent2);
+        profileDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(workAgent2);
+        if (disAllowProfileDefaultNetworkCallback != null) {
+            assertNoCallbacks(disAllowProfileDefaultNetworkCallback);
+        }
         assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
         inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
                 workAgent2.getNetwork().netId, INetd.PERMISSION_SYSTEM));
         inOrder.verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
-                workAgent2.getNetwork().netId, uidRangeFor(testHandle), PREFERENCE_ORDER_PROFILE));
+                workAgent2.getNetwork().netId,
+                uidRangeFor(testHandle, profileNetworkPreference), PREFERENCE_ORDER_PROFILE));
 
         workAgent2.setNetworkValid(true /* isStrictMode */);
         workAgent2.mNetworkMonitor.forceReevaluation(Process.myUid());
-        mProfileDefaultNetworkCallback.expectCapabilitiesThat(workAgent2,
+        profileDefaultNetworkCallback.expectCapabilitiesThat(workAgent2,
                 nc -> nc.hasCapability(NET_CAPABILITY_ENTERPRISE)
-                        && !nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+                        && !nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                        && nc.hasEnterpriseId(
+                        profileNetworkPreference.getPreferenceEnterpriseId())
+                        && nc.getEnterpriseIds().length == 1);
+        if (disAllowProfileDefaultNetworkCallback != null) {
+            assertNoCallbacks(disAllowProfileDefaultNetworkCallback);
+        }
         assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
         inOrder.verify(mMockNetd, never()).networkAddUidRangesParcel(any());
 
         // When the agent disconnects, test that the app on the work profile fall back to the
         // default network.
         workAgent2.disconnect();
-        mProfileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, workAgent2);
+        profileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, workAgent2);
+        if (disAllowProfileDefaultNetworkCallback != null) {
+            assertNoCallbacks(disAllowProfileDefaultNetworkCallback);
+        }
         assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
         inOrder.verify(mMockNetd).networkDestroy(workAgent2.getNetwork().netId);
 
         assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback,
-                mProfileDefaultNetworkCallback);
+                profileDefaultNetworkCallback);
 
         // Callbacks will be unregistered by tearDown()
     }
@@ -13938,11 +14053,15 @@
      */
     @Test
     public void testPreferenceForUserNetworkUpDown() throws Exception {
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        registerDefaultNetworkCallbacks();
         ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
                 new ProfileNetworkPreference.Builder();
         profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
         testPreferenceForUserNetworkUpDownForGivenPreference(
-                profileNetworkPreferenceBuilder.build(), false);
+                profileNetworkPreferenceBuilder.build(), false,
+                testHandle, mProfileDefaultNetworkCallback, null);
     }
 
     /**
@@ -13952,12 +14071,16 @@
      */
     @Test
     public void testPreferenceForUserNetworkUpDownWithNoFallback() throws Exception {
+        final UserHandle testHandle = setupEnterpriseNetwork();
         ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
                 new ProfileNetworkPreference.Builder();
         profileNetworkPreferenceBuilder.setPreference(
                 PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        registerDefaultNetworkCallbacks();
         testPreferenceForUserNetworkUpDownForGivenPreference(
-                profileNetworkPreferenceBuilder.build(), false);
+                profileNetworkPreferenceBuilder.build(), false,
+                testHandle, mProfileDefaultNetworkCallback, null);
     }
 
     /**
@@ -13969,12 +14092,227 @@
     @Test
     public void testPreferenceForUserNetworkUpDownWithNoFallbackWithAlreadyConnectedWorkAgent()
             throws Exception {
+        final UserHandle testHandle = setupEnterpriseNetwork();
         ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
                 new ProfileNetworkPreference.Builder();
         profileNetworkPreferenceBuilder.setPreference(
                 PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        registerDefaultNetworkCallbacks();
         testPreferenceForUserNetworkUpDownForGivenPreference(
-                profileNetworkPreferenceBuilder.build(), true);
+                profileNetworkPreferenceBuilder.build(), true, testHandle,
+                mProfileDefaultNetworkCallback, null);
+    }
+
+    /**
+     * Make sure per-profile networking preference for specific uid of test handle
+     * behaves as expected
+     */
+    @Test
+    public void testPreferenceForDefaultUidOfTestHandle() throws Exception {
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        profileNetworkPreferenceBuilder.setIncludedUids(
+                List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID)));
+        registerDefaultNetworkCallbacks();
+        testPreferenceForUserNetworkUpDownForGivenPreference(
+                profileNetworkPreferenceBuilder.build(), false, testHandle,
+                mProfileDefaultNetworkCallback, null);
+    }
+
+    /**
+     * Make sure per-profile networking preference for specific uid of test handle
+     * behaves as expected
+     */
+    @Test
+    public void testPreferenceForSpecificUidOfOnlyOneApp() throws Exception {
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        profileNetworkPreferenceBuilder.setIncludedUids(
+                List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+        registerDefaultNetworkCallbacks();
+        testPreferenceForUserNetworkUpDownForGivenPreference(
+                profileNetworkPreferenceBuilder.build(), false,
+                testHandle, mProfileDefaultNetworkCallbackAsAppUid2, null);
+    }
+
+    /**
+     * Make sure per-profile networking preference for specific uid of test handle
+     * behaves as expected
+     */
+    @Test
+    public void testPreferenceForDisallowSpecificUidOfApp() throws Exception {
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        profileNetworkPreferenceBuilder.setExcludedUids(
+                List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+        registerDefaultNetworkCallbacks();
+        testPreferenceForUserNetworkUpDownForGivenPreference(
+                profileNetworkPreferenceBuilder.build(), false,
+                testHandle, mProfileDefaultNetworkCallback,
+                mProfileDefaultNetworkCallbackAsAppUid2);
+    }
+
+    /**
+     * Make sure per-profile networking preference for specific uid of test handle
+     * invalid uid inputs
+     */
+    @Test
+    public void testPreferenceForInvalidUids() throws Exception {
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        profileNetworkPreferenceBuilder.setExcludedUids(
+                List.of(testHandle.getUid(0) - 1));
+        final TestOnCompleteListener listener = new TestOnCompleteListener();
+        Assert.assertThrows(IllegalArgumentException.class, () -> mCm.setProfileNetworkPreferences(
+                testHandle, List.of(profileNetworkPreferenceBuilder.build()),
+                r -> r.run(), listener));
+
+        profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder.setIncludedUids(
+                List.of(testHandle.getUid(0) - 1));
+        Assert.assertThrows(IllegalArgumentException.class,
+                () -> mCm.setProfileNetworkPreferences(
+                        testHandle, List.of(profileNetworkPreferenceBuilder.build()),
+                        r -> r.run(), listener));
+
+
+        profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder.setIncludedUids(
+                List.of(testHandle.getUid(0) - 1));
+        profileNetworkPreferenceBuilder.setExcludedUids(
+                List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+        Assert.assertThrows(IllegalArgumentException.class,
+                () -> mCm.setProfileNetworkPreferences(
+                        testHandle, List.of(profileNetworkPreferenceBuilder.build()),
+                        r -> r.run(), listener));
+
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder2 =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder2.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder2.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        profileNetworkPreferenceBuilder2.setIncludedUids(
+                List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+        profileNetworkPreferenceBuilder.setIncludedUids(
+                List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+        Assert.assertThrows(IllegalArgumentException.class,
+                () -> mCm.setProfileNetworkPreferences(
+                        testHandle, List.of(profileNetworkPreferenceBuilder.build(),
+                                profileNetworkPreferenceBuilder2.build()),
+                        r -> r.run(), listener));
+
+        profileNetworkPreferenceBuilder2.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder2.setExcludedUids(
+                List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+        profileNetworkPreferenceBuilder.setExcludedUids(
+                List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+        Assert.assertThrows(IllegalArgumentException.class,
+                () -> mCm.setProfileNetworkPreferences(
+                        testHandle, List.of(profileNetworkPreferenceBuilder.build(),
+                                profileNetworkPreferenceBuilder2.build()),
+                        r -> r.run(), listener));
+
+        profileNetworkPreferenceBuilder2.setPreference(
+                PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+        profileNetworkPreferenceBuilder2.setExcludedUids(
+                List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+        profileNetworkPreferenceBuilder.setExcludedUids(
+                List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+        Assert.assertThrows(IllegalArgumentException.class,
+                () -> mCm.setProfileNetworkPreferences(
+                        testHandle, List.of(profileNetworkPreferenceBuilder.build(),
+                                profileNetworkPreferenceBuilder2.build()),
+                        r -> r.run(), listener));
+    }
+
+    /**
+     * Make sure per-profile networking preference behaves as expected when the enterprise network
+     * goes up and down while the preference is active. Make sure they behave as expected whether
+     * there is a general default network or not when configured to fallback to default network
+     * along with already connected enterprise work agent
+     */
+    @Test
+    public void testPreferenceForUserNetworkUpDownWithFallbackWithAlreadyConnectedWorkAgent()
+            throws Exception {
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        registerDefaultNetworkCallbacks();
+        testPreferenceForUserNetworkUpDownForGivenPreference(
+                profileNetworkPreferenceBuilder.build(), true,
+                testHandle, mProfileDefaultNetworkCallback,
+                null);
+    }
+
+    /**
+     * Make sure per-profile networking preference behaves as expected when the enterprise network
+     * goes up and down while the preference is active for a given enterprise identifier
+     */
+    @Test
+    public void testPreferenceForUserNetworkUpDownWithDefaultEnterpriseId()
+            throws Exception {
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(
+                PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+        registerDefaultNetworkCallbacks();
+        testPreferenceForUserNetworkUpDownForGivenPreference(
+                profileNetworkPreferenceBuilder.build(), true,
+                testHandle, mProfileDefaultNetworkCallback,
+                null);
+    }
+
+    /**
+     * Make sure per-profile networking preference behaves as expected when the enterprise network
+     * goes up and down while the preference is active for a given enterprise identifier
+     */
+    @Test
+    public void testPreferenceForUserNetworkUpDownWithId2()
+            throws Exception {
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(
+                PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(
+                NetworkCapabilities.NET_ENTERPRISE_ID_2);
+        registerDefaultNetworkCallbacks();
+        testPreferenceForUserNetworkUpDownForGivenPreference(
+                profileNetworkPreferenceBuilder.build(), true,
+                testHandle, mProfileDefaultNetworkCallback, null);
+    }
+
+    /**
+     * Make sure per-profile networking preference behaves as expected when the enterprise network
+     * goes up and down while the preference is active for a given enterprise identifier
+     */
+    @Test
+    public void testPreferenceForUserNetworkUpDownWithInvalidId()
+            throws Exception {
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(
+                PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(0);
+        registerDefaultNetworkCallbacks();
+        assertThrows("Should not be able to set invalid enterprise id",
+                IllegalStateException.class, () -> profileNetworkPreferenceBuilder.build());
     }
 
     /**
@@ -14127,10 +14465,15 @@
     public void testProfileNetworkPrefWrongPreference() throws Exception {
         final UserHandle testHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID);
         mServiceContext.setWorkProfile(testHandle, true);
+        ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+                new ProfileNetworkPreference.Builder();
+        profileNetworkPreferenceBuilder.setPreference(
+                PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK + 1);
+        profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
         assertThrows("Should not be able to set an illegal preference",
                 IllegalArgumentException.class,
-                () -> mCm.setProfileNetworkPreference(testHandle,
-                        PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK + 1,
+                () -> mCm.setProfileNetworkPreferences(testHandle,
+                        List.of(profileNetworkPreferenceBuilder.build()),
                         null, null));
     }
 
diff --git a/tests/unit/java/com/android/server/NetworkManagementServiceTest.java b/tests/unit/java/com/android/server/NetworkManagementServiceTest.java
index ea29da0..0c58582 100644
--- a/tests/unit/java/com/android/server/NetworkManagementServiceTest.java
+++ b/tests/unit/java/com/android/server/NetworkManagementServiceTest.java
@@ -22,6 +22,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -32,6 +33,7 @@
 
 import android.annotation.NonNull;
 import android.content.Context;
+import android.net.ConnectivityManager;
 import android.net.INetd;
 import android.net.INetdUnsolicitedEventListener;
 import android.net.LinkAddress;
@@ -71,6 +73,7 @@
 public class NetworkManagementServiceTest {
     private NetworkManagementService mNMService;
     @Mock private Context mContext;
+    @Mock private ConnectivityManager mCm;
     @Mock private IBatteryStats.Stub mBatteryStatsService;
     @Mock private INetd.Stub mNetdService;
 
@@ -113,6 +116,9 @@
         MockitoAnnotations.initMocks(this);
         doNothing().when(mNetdService)
                 .registerUnsolicitedEventListener(mUnsolListenerCaptor.capture());
+        doReturn(Context.CONNECTIVITY_SERVICE).when(mContext).getSystemServiceName(
+                eq(ConnectivityManager.class));
+        doReturn(mCm).when(mContext).getSystemService(eq(Context.CONNECTIVITY_SERVICE));
         // Start the service and wait until it connects to our socket.
         mNMService = NetworkManagementService.create(mContext, mDeps);
     }
@@ -239,6 +245,7 @@
         mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, true);
         assertTrue("Should be true since mobile data usage is restricted",
                 mNMService.isNetworkRestricted(TEST_UID));
+        verify(mCm).updateMeteredNetworkDenyList(TEST_UID, true /* enabled */);
 
         mNMService.setDataSaverModeEnabled(true);
         verify(mNetdService).bandwidthEnableDataSaver(true);
@@ -246,13 +253,16 @@
         mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, false);
         assertTrue("Should be true since data saver is on and the uid is not allowlisted",
                 mNMService.isNetworkRestricted(TEST_UID));
+        verify(mCm).updateMeteredNetworkDenyList(TEST_UID, true /* false */);
 
         mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, true);
         assertFalse("Should be false since data saver is on and the uid is allowlisted",
                 mNMService.isNetworkRestricted(TEST_UID));
+        verify(mCm).updateMeteredNetworkAllowList(TEST_UID, true /* enabled */);
 
         // remove uid from allowlist and turn datasaver off again
         mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, false);
+        verify(mCm).updateMeteredNetworkAllowList(TEST_UID, false /* enabled */);
         mNMService.setDataSaverModeEnabled(false);
         verify(mNetdService).bandwidthEnableDataSaver(false);
         assertFalse("Network should not be restricted when data saver is off",
@@ -306,12 +316,14 @@
         for (int chain : chains) {
             final ArrayMap<Integer, Boolean> expectedValues = expected.get(chain);
             mNMService.setFirewallChainEnabled(chain, true);
+            verify(mCm).setFirewallChainEnabled(chain, true /* enabled */);
             for (int state : states) {
                 mNMService.setFirewallUidRule(chain, TEST_UID, state);
                 assertEquals(errorMsg.apply(chain, state),
                         expectedValues.get(state), mNMService.isNetworkRestricted(TEST_UID));
             }
             mNMService.setFirewallChainEnabled(chain, false);
+            verify(mCm).setFirewallChainEnabled(chain, false /* enabled */);
         }
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
new file mode 100644
index 0000000..84e02ce
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2022 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.server.connectivity;
+
+import static android.net.INetd.IF_STATE_UP;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_MTU;
+import static com.android.server.connectivity.ClatCoordinator.CLAT_MAX_MTU;
+import static com.android.server.connectivity.ClatCoordinator.INIT_V4ADDR_PREFIX_LEN;
+import static com.android.server.connectivity.ClatCoordinator.INIT_V4ADDR_STRING;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+
+import android.annotation.NonNull;
+import android.net.INetd;
+import android.net.IpPrefix;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.util.Objects;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+public class ClatCoordinatorTest {
+    private static final String BASE_IFACE = "test0";
+    private static final String STACKED_IFACE = "v4-test0";
+    private static final int BASE_IFINDEX = 1000;
+
+    private static final IpPrefix NAT64_IP_PREFIX = new IpPrefix("64:ff9b::/96");
+    private static final String NAT64_PREFIX_STRING = "64:ff9b::";
+    private static final int GOOGLE_DNS_4 = 0x08080808;  // 8.8.8.8
+    private static final int NETID = 42;
+
+    // The test fwmark means: PERMISSION_SYSTEM (0x2), protectedFromVpn: true,
+    // explicitlySelected: true, netid: 42. For bit field structure definition, see union Fwmark in
+    // system/netd/include/Fwmark.h
+    private static final int MARK = 0xb002a;
+
+    private static final String XLAT_LOCAL_IPV4ADDR_STRING = "192.0.0.46";
+    private static final String XLAT_LOCAL_IPV6ADDR_STRING = "2001:db8:0:b11::464";
+    private static final int CLATD_PID = 10483;
+
+    private static final int TUN_FD = 534;
+    private static final int RAW_SOCK_FD = 535;
+    private static final int PACKET_SOCK_FD = 536;
+    private static final ParcelFileDescriptor TUN_PFD = new ParcelFileDescriptor(
+            new FileDescriptor());
+    private static final ParcelFileDescriptor RAW_SOCK_PFD = new ParcelFileDescriptor(
+            new FileDescriptor());
+    private static final ParcelFileDescriptor PACKET_SOCK_PFD = new ParcelFileDescriptor(
+            new FileDescriptor());
+
+    @Mock private INetd mNetd;
+    @Spy private TestDependencies mDeps = new TestDependencies();
+
+    /**
+      * The dependency injection class is used to mock the JNI functions and system functions
+      * for clatd coordinator control plane. Note that any testing used JNI functions need to
+      * be overridden to avoid calling native methods.
+      */
+    protected class TestDependencies extends ClatCoordinator.Dependencies {
+        /**
+          * Get netd.
+          */
+        @Override
+        public INetd getNetd() {
+            return mNetd;
+        }
+
+        /**
+         * @see ParcelFileDescriptor#adoptFd(int).
+         */
+        @Override
+        public ParcelFileDescriptor adoptFd(int fd) {
+            switch (fd) {
+                case TUN_FD:
+                    return TUN_PFD;
+                case RAW_SOCK_FD:
+                    return RAW_SOCK_PFD;
+                case PACKET_SOCK_FD:
+                    return PACKET_SOCK_PFD;
+                default:
+                    fail("unsupported arg: " + fd);
+                    return null;
+            }
+        }
+
+        /**
+         * Get interface index for a given interface.
+         */
+        @Override
+        public int getInterfaceIndex(String ifName) {
+            if (BASE_IFACE.equals(ifName)) {
+                return BASE_IFINDEX;
+            }
+            fail("unsupported arg: " + ifName);
+            return -1;
+        }
+
+        /**
+         * Create tun interface for a given interface name.
+         */
+        @Override
+        public int createTunInterface(@NonNull String tuniface) throws IOException {
+            if (STACKED_IFACE.equals(tuniface)) {
+                return TUN_FD;
+            }
+            fail("unsupported arg: " + tuniface);
+            return -1;
+        }
+
+        /**
+         * Pick an IPv4 address for clat.
+         */
+        @Override
+        public String selectIpv4Address(@NonNull String v4addr, int prefixlen)
+                throws IOException {
+            if (INIT_V4ADDR_STRING.equals(v4addr) && INIT_V4ADDR_PREFIX_LEN == prefixlen) {
+                return XLAT_LOCAL_IPV4ADDR_STRING;
+            }
+            fail("unsupported args: " + v4addr + ", " + prefixlen);
+            return null;
+        }
+
+        /**
+         * Generate a checksum-neutral IID.
+         */
+        @Override
+        public String generateIpv6Address(@NonNull String iface, @NonNull String v4,
+                @NonNull String prefix64) throws IOException {
+            if (BASE_IFACE.equals(iface) && XLAT_LOCAL_IPV4ADDR_STRING.equals(v4)
+                    && NAT64_PREFIX_STRING.equals(prefix64)) {
+                return XLAT_LOCAL_IPV6ADDR_STRING;
+            }
+            fail("unsupported args: " + iface + ", " + v4 + ", " + prefix64);
+            return null;
+        }
+
+        /**
+         * Detect MTU.
+         */
+        @Override
+        public int detectMtu(@NonNull String platSubnet, int platSuffix, int mark)
+                throws IOException {
+            if (NAT64_PREFIX_STRING.equals(platSubnet) && GOOGLE_DNS_4 == platSuffix
+                    && MARK == mark) {
+                return ETHER_MTU;
+            }
+            fail("unsupported args: " + platSubnet + ", " + platSuffix + ", " + mark);
+            return -1;
+        }
+
+        /**
+         * Open IPv6 raw socket and set SO_MARK.
+         */
+        @Override
+        public int openRawSocket6(int mark) throws IOException {
+            if (mark == MARK) {
+                return RAW_SOCK_FD;
+            }
+            fail("unsupported arg: " + mark);
+            return -1;
+        }
+
+        /**
+         * Open packet socket.
+         */
+        @Override
+        public int openPacketSocket() throws IOException {
+            // assume that open socket always successfully because there is no argument to check.
+            return PACKET_SOCK_FD;
+        }
+
+        /**
+         * Add anycast setsockopt.
+         */
+        @Override
+        public void addAnycastSetsockopt(@NonNull FileDescriptor sock, String v6, int ifindex)
+                throws IOException {
+            if (Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), sock)
+                    && XLAT_LOCAL_IPV6ADDR_STRING.equals(v6)
+                    && BASE_IFINDEX == ifindex) return;
+            fail("unsupported args: " + sock + ", " + v6 + ", " + ifindex);
+        }
+
+        /**
+         * Configure packet socket.
+         */
+        @Override
+        public void configurePacketSocket(@NonNull FileDescriptor sock, String v6, int ifindex)
+                throws IOException {
+            if (Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), sock)
+                    && XLAT_LOCAL_IPV6ADDR_STRING.equals(v6)
+                    && BASE_IFINDEX == ifindex) return;
+            fail("unsupported args: " + sock + ", " + v6 + ", " + ifindex);
+        }
+
+        /**
+         * Start clatd.
+         */
+        @Override
+        public int startClatd(@NonNull FileDescriptor tunfd, @NonNull FileDescriptor readsock6,
+                @NonNull FileDescriptor writesock6, @NonNull String iface, @NonNull String pfx96,
+                @NonNull String v4, @NonNull String v6) throws IOException {
+            if (Objects.equals(TUN_PFD.getFileDescriptor(), tunfd)
+                    && Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), readsock6)
+                    && Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), writesock6)
+                    && BASE_IFACE.equals(iface)
+                    && NAT64_PREFIX_STRING.equals(pfx96)
+                    && XLAT_LOCAL_IPV4ADDR_STRING.equals(v4)
+                    && XLAT_LOCAL_IPV6ADDR_STRING.equals(v6)) {
+                return CLATD_PID;
+            }
+            fail("unsupported args: " + tunfd + ", " + readsock6 + ", " + writesock6 + ", "
+                    + ", " + iface + ", " + v4 + ", " + v6);
+            return -1;
+        }
+
+        /**
+         * Stop clatd.
+         */
+        public void stopClatd(@NonNull String iface, @NonNull String pfx96, @NonNull String v4,
+                @NonNull String v6, int pid) throws IOException {
+            if (pid == -1) {
+                fail("unsupported arg: " + pid);
+            }
+        }
+    };
+
+    @NonNull
+    private ClatCoordinator makeClatCoordinator() throws Exception {
+        final ClatCoordinator coordinator = new ClatCoordinator(mDeps);
+        return coordinator;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    private boolean assertContainsFlag(String[] flags, String match) {
+        for (String flag : flags) {
+            if (flag.equals(match)) return true;
+        }
+        fail("Missing flag: " + match);
+        return false;
+    }
+
+    @Test
+    public void testStartStopClatd() throws Exception {
+        final ClatCoordinator coordinator = makeClatCoordinator();
+        final InOrder inOrder = inOrder(mNetd, mDeps);
+        clearInvocations(mNetd, mDeps);
+
+        // [1] Start clatd.
+        final String addr6For464xlat = coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX);
+        assertEquals(XLAT_LOCAL_IPV6ADDR_STRING, addr6For464xlat);
+
+        // Pick an IPv4 address.
+        inOrder.verify(mDeps).selectIpv4Address(eq(INIT_V4ADDR_STRING),
+                eq(INIT_V4ADDR_PREFIX_LEN));
+
+        // Generate a checksum-neutral IID.
+        inOrder.verify(mDeps).generateIpv6Address(eq(BASE_IFACE),
+                eq(XLAT_LOCAL_IPV4ADDR_STRING), eq(NAT64_PREFIX_STRING));
+
+        // Open, configure and bring up the tun interface.
+        inOrder.verify(mDeps).createTunInterface(eq(STACKED_IFACE));
+        inOrder.verify(mDeps).adoptFd(eq(TUN_FD));
+        inOrder.verify(mNetd).interfaceSetEnableIPv6(eq(STACKED_IFACE), eq(false /* enable */));
+        inOrder.verify(mDeps).detectMtu(eq(NAT64_PREFIX_STRING), eq(GOOGLE_DNS_4), eq(MARK));
+        inOrder.verify(mNetd).interfaceSetMtu(eq(STACKED_IFACE),
+                eq(1472 /* ETHER_MTU(1500) - MTU_DELTA(28) */));
+        inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
+                STACKED_IFACE.equals(cfg.ifName)
+                && XLAT_LOCAL_IPV4ADDR_STRING.equals(cfg.ipv4Addr)
+                && (32 == cfg.prefixLength)
+                && "".equals(cfg.hwAddr)
+                && assertContainsFlag(cfg.flags, IF_STATE_UP)));
+
+        // Open and configure 464xlat read/write sockets.
+        inOrder.verify(mDeps).openPacketSocket();
+        inOrder.verify(mDeps).adoptFd(eq(PACKET_SOCK_FD));
+        inOrder.verify(mDeps).openRawSocket6(eq(MARK));
+        inOrder.verify(mDeps).adoptFd(eq(RAW_SOCK_FD));
+        inOrder.verify(mDeps).getInterfaceIndex(eq(BASE_IFACE));
+        inOrder.verify(mDeps).addAnycastSetsockopt(
+                argThat(fd -> Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), fd)),
+                eq(XLAT_LOCAL_IPV6ADDR_STRING), eq(BASE_IFINDEX));
+        inOrder.verify(mDeps).configurePacketSocket(
+                argThat(fd -> Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), fd)),
+                eq(XLAT_LOCAL_IPV6ADDR_STRING), eq(BASE_IFINDEX));
+
+        // Start clatd.
+        inOrder.verify(mDeps).startClatd(
+                argThat(fd -> Objects.equals(TUN_PFD.getFileDescriptor(), fd)),
+                argThat(fd -> Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), fd)),
+                argThat(fd -> Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), fd)),
+                eq(BASE_IFACE), eq(NAT64_PREFIX_STRING),
+                eq(XLAT_LOCAL_IPV4ADDR_STRING), eq(XLAT_LOCAL_IPV6ADDR_STRING));
+        inOrder.verifyNoMoreInteractions();
+
+        // [2] Start clatd again failed.
+        assertThrows("java.io.IOException: Clatd is already running on test0 (pid 10483)",
+                IOException.class,
+                () -> coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX));
+
+        // [3] Expect clatd to stop successfully.
+        coordinator.clatStop();
+        inOrder.verify(mDeps).stopClatd(eq(BASE_IFACE), eq(NAT64_PREFIX_STRING),
+                eq(XLAT_LOCAL_IPV4ADDR_STRING), eq(XLAT_LOCAL_IPV6ADDR_STRING), eq(CLATD_PID));
+        inOrder.verifyNoMoreInteractions();
+
+        // [4] Expect an IO exception while stopping a clatd that doesn't exist.
+        assertThrows("java.io.IOException: Clatd has not started", IOException.class,
+                () -> coordinator.clatStop());
+        inOrder.verify(mDeps, never()).stopClatd(anyString(), anyString(), anyString(),
+                anyString(), anyInt());
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    public void testGetFwmark() throws Exception {
+        assertEquals(0xb0064, ClatCoordinator.getFwmark(100));
+        assertEquals(0xb03e8, ClatCoordinator.getFwmark(1000));
+        assertEquals(0xb2710, ClatCoordinator.getFwmark(10000));
+        assertEquals(0xbffff, ClatCoordinator.getFwmark(65535));
+    }
+
+    @Test
+    public void testAdjustMtu() throws Exception {
+        // Expected mtu is that IPV6_MIN_MTU(1280) minus MTU_DELTA(28).
+        assertEquals(1252, ClatCoordinator.adjustMtu(-1 /* detect mtu failed */));
+        assertEquals(1252, ClatCoordinator.adjustMtu(500));
+        assertEquals(1252, ClatCoordinator.adjustMtu(1000));
+        assertEquals(1252, ClatCoordinator.adjustMtu(1280));
+
+        // Expected mtu is that the detected mtu minus MTU_DELTA(28).
+        assertEquals(1372, ClatCoordinator.adjustMtu(1400));
+        assertEquals(1472, ClatCoordinator.adjustMtu(ETHER_MTU));
+        assertEquals(65508, ClatCoordinator.adjustMtu(CLAT_MAX_MTU));
+
+        // Expected mtu is that CLAT_MAX_MTU(65536) minus MTU_DELTA(28).
+        assertEquals(65508, ClatCoordinator.adjustMtu(CLAT_MAX_MTU + 1 /* over maximum mtu */));
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
index c86e699..ec51537 100644
--- a/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
@@ -296,7 +296,7 @@
                 false /* roaming */);
 
         verify(mStatsManager, times(1)).registerUsageCallback(
-                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
+                any(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
     }
 
     @Test
@@ -315,7 +315,7 @@
 
         // Daily budget should be 15MB (5% of daily quota), 7MB used today: callback set for 8MB
         verify(mStatsManager, times(1)).registerUsageCallback(
-                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
+                any(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
     }
 
     @Test
@@ -334,7 +334,7 @@
 
         // Daily budget should be 15MB (5% of daily quota), 7MB used today: callback set for 8MB
         verify(mStatsManager, times(1)).registerUsageCallback(
-                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
+                any(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
     }
 
     @Test
@@ -351,7 +351,7 @@
 
         // Default global setting should be used: 12 - 7 = 5
         verify(mStatsManager, times(1)).registerUsageCallback(
-                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(5)), any(), any());
+                any(), eq(DataUnit.MEGABYTES.toBytes(5)), any(), any());
     }
 
     @Test
@@ -366,7 +366,7 @@
                 false /* roaming */);
 
         verify(mStatsManager, times(1)).registerUsageCallback(
-                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
+                any(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
 
         // Update setting
         setDefaultQuotaGlobalSetting(DataUnit.MEGABYTES.toBytes(14));
@@ -376,7 +376,7 @@
         // Callback must have been re-registered with new setting
         verify(mStatsManager, times(1)).unregisterUsageCallback(any());
         verify(mStatsManager, times(1)).registerUsageCallback(
-                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
+                any(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
     }
 
     @Test
@@ -391,7 +391,7 @@
                 false /* roaming */);
 
         verify(mStatsManager, times(1)).registerUsageCallback(
-                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
+                any(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
 
         when(mResources.getInteger(R.integer.config_networkDefaultDailyMultipathQuotaBytes))
                 .thenReturn((int) DataUnit.MEGABYTES.toBytes(16));
@@ -402,6 +402,6 @@
 
         // Uses the new setting (16 - 2 = 14MB)
         verify(mStatsManager, times(1)).registerUsageCallback(
-                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(14)), any(), any());
+                any(), eq(DataUnit.MEGABYTES.toBytes(14)), any(), any());
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/UidRangeUtilsTest.java b/tests/unit/java/com/android/server/connectivity/UidRangeUtilsTest.java
new file mode 100644
index 0000000..b8c2673
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/UidRangeUtilsTest.java
@@ -0,0 +1,354 @@
+/*
+ * Copyright (C) 2022 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.server.connectivity;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.UidRange;
+import android.os.Build;
+import android.util.ArraySet;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Tests for UidRangeUtils.
+ *
+ * Build, install and run with:
+ *  runtest frameworks-net -c com.android.server.connectivity.UidRangeUtilsTest
+ */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+public class UidRangeUtilsTest {
+    private static void assertInSameRange(@NonNull final String msg,
+            @Nullable final UidRange r1,
+            @Nullable final Set<UidRange> s2) {
+        assertTrue(msg + " : " + s2 + " unexpectedly is not in range of " + r1,
+                UidRangeUtils.isRangeSetInUidRange(r1, s2));
+    }
+
+    private static void assertNotInSameRange(@NonNull final String msg,
+            @Nullable final UidRange r1, @Nullable final Set<UidRange> s2) {
+        assertFalse(msg + " : " + s2 + " unexpectedly is in range of " + r1,
+                UidRangeUtils.isRangeSetInUidRange(r1, s2));
+    }
+
+    @Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testRangeSetInUidRange() {
+        final UidRange uids1 = new UidRange(1, 100);
+        final UidRange uids2 = new UidRange(3, 300);
+        final UidRange uids3 = new UidRange(1, 1000);
+        final UidRange uids4 = new UidRange(1, 100);
+        final UidRange uids5 = new UidRange(2, 20);
+        final UidRange uids6 = new UidRange(3, 30);
+
+        assertThrows(NullPointerException.class,
+                () -> UidRangeUtils.isRangeSetInUidRange(null, null));
+        assertThrows(NullPointerException.class,
+                () -> UidRangeUtils.isRangeSetInUidRange(uids1, null));
+
+        final ArraySet<UidRange> set1 = new ArraySet<>();
+        final ArraySet<UidRange> set2 = new ArraySet<>();
+
+        assertThrows(NullPointerException.class,
+                () -> UidRangeUtils.isRangeSetInUidRange(null, set1));
+        assertInSameRange("uids1 <=> empty", uids1, set2);
+
+        set2.add(uids1);
+        assertInSameRange("uids1 <=> uids1", uids1, set2);
+
+        set2.clear();
+        set2.add(uids2);
+        assertNotInSameRange("uids1 <=> uids2", uids1, set2);
+        set2.clear();
+        set2.add(uids3);
+        assertNotInSameRange("uids1 <=> uids3", uids1, set2);
+        set2.clear();
+        set2.add(uids4);
+        assertInSameRange("uids1 <=> uids4", uids1, set2);
+
+        set2.clear();
+        set2.add(uids5);
+        set2.add(uids6);
+        assertInSameRange("uids1 <=> uids5, 6", uids1, set2);
+
+        set2.clear();
+        set2.add(uids2);
+        set2.add(uids6);
+        assertNotInSameRange("uids1 <=> uids2, 6", uids1, set2);
+    }
+
+    @Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testRemoveRangeSetFromUidRange() {
+        final UidRange uids1 = new UidRange(1, 100);
+        final UidRange uids2 = new UidRange(3, 300);
+        final UidRange uids3 = new UidRange(1, 1000);
+        final UidRange uids4 = new UidRange(1, 100);
+        final UidRange uids5 = new UidRange(2, 20);
+        final UidRange uids6 = new UidRange(3, 30);
+        final UidRange uids7 = new UidRange(30, 39);
+
+        final UidRange uids8 = new UidRange(1, 1);
+        final UidRange uids9 = new UidRange(21, 100);
+        final UidRange uids10 = new UidRange(1, 2);
+        final UidRange uids11 = new UidRange(31, 100);
+
+        final UidRange uids12 = new UidRange(1, 1);
+        final UidRange uids13 = new UidRange(21, 29);
+        final UidRange uids14 = new UidRange(40, 100);
+
+        final UidRange uids15 = new UidRange(3, 30);
+        final UidRange uids16 = new UidRange(31, 39);
+
+        assertThrows(NullPointerException.class,
+                () -> UidRangeUtils.removeRangeSetFromUidRange(null, null));
+        Set<UidRange> expected = new ArraySet<>();
+        expected.add(uids1);
+        assertThrows(NullPointerException.class,
+                () -> UidRangeUtils.removeRangeSetFromUidRange(uids1, null));
+        assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, new ArraySet<>()));
+
+        expected.clear();
+        final ArraySet<UidRange> set2 = new ArraySet<>();
+        set2.add(uids1);
+        assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2));
+        set2.clear();
+        set2.add(uids4);
+        assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2));
+
+        expected.add(uids10);
+        set2.clear();
+        set2.add(uids2);
+        assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2));
+
+        expected.clear();
+        set2.clear();
+        set2.add(uids3);
+        assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2));
+
+        set2.clear();
+        set2.add(uids3);
+        set2.add(uids6);
+        assertThrows(IllegalArgumentException.class,
+                () -> UidRangeUtils.removeRangeSetFromUidRange(uids1, set2));
+
+        expected.clear();
+        expected.add(uids8);
+        expected.add(uids9);
+        set2.clear();
+        set2.add(uids5);
+        assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2));
+
+        expected.clear();
+        expected.add(uids10);
+        expected.add(uids11);
+        set2.clear();
+        set2.add(uids6);
+        assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2));
+
+        expected.clear();
+        expected.add(uids12);
+        expected.add(uids13);
+        expected.add(uids14);
+        set2.clear();
+        set2.add(uids5);
+        set2.add(uids7);
+        assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2));
+
+        expected.clear();
+        expected.add(uids10);
+        expected.add(uids14);
+        set2.clear();
+        set2.add(uids15);
+        set2.add(uids16);
+        assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2));
+    }
+
+    private static void assertRangeOverlaps(@NonNull final String msg,
+            @Nullable final Set<UidRange> s1,
+            @Nullable final Set<UidRange> s2) {
+        assertTrue(msg + " : " + s2 + " unexpectedly does not overlap with " + s1,
+                UidRangeUtils.doesRangeSetOverlap(s1, s2));
+    }
+
+    private static void assertRangeDoesNotOverlap(@NonNull final String msg,
+            @Nullable final Set<UidRange> s1, @Nullable final Set<UidRange> s2) {
+        assertFalse(msg + " : " + s2 + " unexpectedly ovelaps with " + s1,
+                UidRangeUtils.doesRangeSetOverlap(s1, s2));
+    }
+
+    @Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testRangeSetOverlap() {
+        final UidRange uids1 = new UidRange(1, 100);
+        final UidRange uids2 = new UidRange(3, 300);
+        final UidRange uids3 = new UidRange(1, 1000);
+        final UidRange uids4 = new UidRange(1, 100);
+        final UidRange uids5 = new UidRange(2, 20);
+        final UidRange uids6 = new UidRange(3, 30);
+        final UidRange uids7 = new UidRange(0, 0);
+        final UidRange uids8 = new UidRange(1, 500);
+        final UidRange uids9 = new UidRange(101, 200);
+
+        assertThrows(NullPointerException.class,
+                () -> UidRangeUtils.doesRangeSetOverlap(null, null));
+
+        final ArraySet<UidRange> set1 = new ArraySet<>();
+        final ArraySet<UidRange> set2 = new ArraySet<>();
+        assertThrows(NullPointerException.class,
+                () -> UidRangeUtils.doesRangeSetOverlap(set1, null));
+        assertThrows(NullPointerException.class,
+                () -> UidRangeUtils.doesRangeSetOverlap(null, set2));
+        assertRangeDoesNotOverlap("empty <=> null", set1, set2);
+
+        set2.add(uids1);
+        set1.add(uids1);
+        assertRangeOverlaps("uids1 <=> uids1", set1, set2);
+
+        set1.clear();
+        set1.add(uids1);
+        set2.clear();
+        set2.add(uids2);
+        assertRangeOverlaps("uids1 <=> uids2", set1, set2);
+
+        set1.clear();
+        set1.add(uids1);
+        set2.clear();
+        set2.add(uids3);
+        assertRangeOverlaps("uids1 <=> uids3", set1, set2);
+
+        set1.clear();
+        set1.add(uids1);
+        set2.clear();
+        set2.add(uids4);
+        assertRangeOverlaps("uids1 <=> uids4", set1, set2);
+
+        set1.clear();
+        set1.add(uids1);
+        set2.clear();
+        set2.add(uids5);
+        set2.add(uids6);
+        assertRangeOverlaps("uids1 <=> uids5,6", set1, set2);
+
+        set1.clear();
+        set1.add(uids1);
+        set2.clear();
+        set2.add(uids7);
+        assertRangeDoesNotOverlap("uids1 <=> uids7", set1, set2);
+
+        set1.clear();
+        set1.add(uids1);
+        set2.clear();
+        set2.add(uids9);
+        assertRangeDoesNotOverlap("uids1 <=> uids9", set1, set2);
+
+        set1.clear();
+        set1.add(uids1);
+        set2.clear();
+        set2.add(uids8);
+        assertRangeOverlaps("uids1 <=> uids8", set1, set2);
+
+
+        set1.clear();
+        set1.add(uids1);
+        set2.clear();
+        set2.add(uids8);
+        set2.add(uids7);
+        assertRangeOverlaps("uids1 <=> uids7, 8", set1, set2);
+    }
+
+    @Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testConvertListToUidRange() {
+        final UidRange uids1 = new UidRange(1, 1);
+        final UidRange uids2 = new UidRange(1, 2);
+        final UidRange uids3 = new UidRange(100, 100);
+        final UidRange uids4 = new UidRange(10, 10);
+
+        final UidRange uids5 = new UidRange(10, 14);
+        final UidRange uids6 = new UidRange(20, 24);
+
+        final Set<UidRange> expected = new ArraySet<>();
+        final List<Integer> input = new ArrayList<Integer>();
+
+        assertThrows(NullPointerException.class, () -> UidRangeUtils.convertListToUidRange(null));
+        assertEquals(expected, UidRangeUtils.convertListToUidRange(input));
+
+        input.add(1);
+        expected.add(uids1);
+        assertEquals(expected, UidRangeUtils.convertListToUidRange(input));
+
+        input.add(2);
+        expected.clear();
+        expected.add(uids2);
+        assertEquals(expected, UidRangeUtils.convertListToUidRange(input));
+
+        input.clear();
+        input.add(1);
+        input.add(100);
+        expected.clear();
+        expected.add(uids1);
+        expected.add(uids3);
+        assertEquals(expected, UidRangeUtils.convertListToUidRange(input));
+
+        input.clear();
+        input.add(100);
+        input.add(1);
+        expected.clear();
+        expected.add(uids1);
+        expected.add(uids3);
+        assertEquals(expected, UidRangeUtils.convertListToUidRange(input));
+
+        input.clear();
+        input.add(100);
+        input.add(1);
+        input.add(2);
+        input.add(1);
+        input.add(10);
+        expected.clear();
+        expected.add(uids2);
+        expected.add(uids4);
+        expected.add(uids3);
+        assertEquals(expected, UidRangeUtils.convertListToUidRange(input));
+
+        input.clear();
+        input.add(10);
+        input.add(11);
+        input.add(12);
+        input.add(13);
+        input.add(14);
+        input.add(20);
+        input.add(21);
+        input.add(22);
+        input.add(23);
+        input.add(24);
+        expected.clear();
+        expected.add(uids5);
+        expected.add(uids6);
+        assertEquals(expected, UidRangeUtils.convertListToUidRange(input));
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java b/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
new file mode 100644
index 0000000..987b7b7
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2022 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.server.net;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.net.INetd;
+import android.net.MacAddress;
+import android.os.Handler;
+import android.os.test.TestLooper;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.InterfaceParams;
+import com.android.net.module.util.Struct.U32;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class BpfInterfaceMapUpdaterTest {
+    private static final int TEST_INDEX = 1;
+    private static final int TEST_INDEX2 = 2;
+    private static final String TEST_INTERFACE_NAME = "test1";
+    private static final String TEST_INTERFACE_NAME2 = "test2";
+
+    private final TestLooper mLooper = new TestLooper();
+    private BaseNetdUnsolicitedEventListener mListener;
+    private BpfInterfaceMapUpdater mUpdater;
+    @Mock private IBpfMap<U32, InterfaceMapValue> mBpfMap;
+    @Mock private INetd mNetd;
+    @Mock private Context mContext;
+
+    private class TestDependencies extends BpfInterfaceMapUpdater.Dependencies {
+        @Override
+        public IBpfMap<U32, InterfaceMapValue> getInterfaceMap() {
+            return mBpfMap;
+        }
+
+        @Override
+        public InterfaceParams getInterfaceParams(String ifaceName) {
+            if (ifaceName.equals(TEST_INTERFACE_NAME)) {
+                return new InterfaceParams(TEST_INTERFACE_NAME, TEST_INDEX,
+                        MacAddress.ALL_ZEROS_ADDRESS);
+            } else if (ifaceName.equals(TEST_INTERFACE_NAME2)) {
+                return new InterfaceParams(TEST_INTERFACE_NAME2, TEST_INDEX2,
+                        MacAddress.ALL_ZEROS_ADDRESS);
+            }
+
+            return null;
+        }
+
+        @Override
+        public INetd getINetd(Context ctx) {
+            return mNetd;
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        when(mNetd.interfaceGetList()).thenReturn(new String[] {TEST_INTERFACE_NAME});
+        mUpdater = new BpfInterfaceMapUpdater(mContext, new Handler(mLooper.getLooper()),
+                new TestDependencies());
+    }
+
+    private void verifyStartUpdater() throws Exception {
+        mUpdater.start();
+        mLooper.dispatchAll();
+        final ArgumentCaptor<BaseNetdUnsolicitedEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(BaseNetdUnsolicitedEventListener.class);
+        verify(mNetd).registerUnsolicitedEventListener(listenerCaptor.capture());
+        mListener = listenerCaptor.getValue();
+        verify(mBpfMap).updateEntry(eq(new U32(TEST_INDEX)),
+                eq(new InterfaceMapValue(TEST_INTERFACE_NAME)));
+    }
+
+    @Test
+    public void testUpdateInterfaceMap() throws Exception {
+        verifyStartUpdater();
+
+        mListener.onInterfaceAdded(TEST_INTERFACE_NAME2);
+        mLooper.dispatchAll();
+        verify(mBpfMap).updateEntry(eq(new U32(TEST_INDEX2)),
+                eq(new InterfaceMapValue(TEST_INTERFACE_NAME2)));
+
+        // Check that when onInterfaceRemoved is called, nothing happens.
+        mListener.onInterfaceRemoved(TEST_INTERFACE_NAME);
+        mLooper.dispatchAll();
+        verifyNoMoreInteractions(mBpfMap);
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
index 8340a13..0e2c293 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
@@ -32,9 +32,12 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
 
+import android.content.Context;
 import android.content.res.Resources;
-import android.net.INetd;
+import android.net.ConnectivityManager;
 import android.net.NetworkStats;
 import android.net.TrafficStats;
 import android.net.UnderlyingNetworkInfo;
@@ -73,8 +76,8 @@
 
     private File mTestProc;
     private NetworkStatsFactory mFactory;
-    @Mock
-    private INetd mNetd;
+    @Mock private Context mContext;
+    @Mock private ConnectivityManager mCm;
 
     @Before
     public void setUp() throws Exception {
@@ -85,7 +88,10 @@
         // applications. So in order to have a test support native library, the native code
         // related to networkStatsFactory is compiled to a minimal native library and loaded here.
         System.loadLibrary("networkstatsfactorytestjni");
-        mFactory = new NetworkStatsFactory(mTestProc, false, mNetd);
+        doReturn(Context.CONNECTIVITY_SERVICE).when(mContext).getSystemServiceName(
+                eq(ConnectivityManager.class));
+        doReturn(mCm).when(mContext).getSystemService(eq(Context.CONNECTIVITY_SERVICE));
+        mFactory = new NetworkStatsFactory(mContext, mTestProc, false);
         mFactory.updateUnderlyingNetworkInfos(new UnderlyingNetworkInfo[0]);
     }
 
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
index 416549c..66dcf6d 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
@@ -29,25 +29,22 @@
 import static android.net.TrafficStats.MB_IN_BYTES;
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyInt;
 
-import android.app.usage.NetworkStatsManager;
 import android.net.DataUsageRequest;
 import android.net.NetworkIdentity;
 import android.net.NetworkIdentitySet;
 import android.net.NetworkStats;
 import android.net.NetworkStatsAccess;
 import android.net.NetworkTemplate;
-import android.os.Build;
-import android.os.ConditionVariable;
-import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.Looper;
-import android.os.Messenger;
 import android.os.Process;
 import android.os.UserHandle;
 import android.telephony.TelephonyManager;
@@ -55,7 +52,6 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.server.net.NetworkStatsServiceTest.LatchedHandler;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
@@ -74,7 +70,7 @@
  */
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
 public class NetworkStatsObserversTest {
     private static final String TEST_IFACE = "test0";
     private static final String TEST_IFACE2 = "test1";
@@ -96,21 +92,15 @@
     private static final long WAIT_TIMEOUT_MS = 500;
     private static final long THRESHOLD_BYTES = 2 * MB_IN_BYTES;
     private static final long BASE_BYTES = 7 * MB_IN_BYTES;
-    private static final int INVALID_TYPE = -1;
-
-    private long mElapsedRealtime;
 
     private HandlerThread mObserverHandlerThread;
-    private Handler mObserverNoopHandler;
-
-    private LatchedHandler mHandler;
 
     private NetworkStatsObservers mStatsObservers;
-    private Messenger mMessenger;
     private ArrayMap<String, NetworkIdentitySet> mActiveIfaces;
     private ArrayMap<String, NetworkIdentitySet> mActiveUidIfaces;
 
-    @Mock private IBinder mockBinder;
+    @Mock private IBinder mUsageCallbackBinder;
+    private TestableUsageCallback mUsageCallback;
 
     @Before
     public void setUp() throws Exception {
@@ -126,24 +116,29 @@
             }
         };
 
-        mHandler = new LatchedHandler(Looper.getMainLooper(), new ConditionVariable());
-        mMessenger = new Messenger(mHandler);
-
         mActiveIfaces = new ArrayMap<>();
         mActiveUidIfaces = new ArrayMap<>();
+        mUsageCallback = new TestableUsageCallback(mUsageCallbackBinder);
     }
 
     @Test
     public void testRegister_thresholdTooLow_setsDefaultThreshold() throws Exception {
-        long thresholdTooLowBytes = 1L;
-        DataUsageRequest inputRequest = new DataUsageRequest(
+        final long thresholdTooLowBytes = 1L;
+        final DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, thresholdTooLowBytes);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
-                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
-        assertTrue(request.requestId > 0);
-        assertTrue(Objects.equals(sTemplateWifi, request.template));
-        assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
+        final DataUsageRequest requestByApp = mStatsObservers.register(inputRequest, mUsageCallback,
+                UID_RED, NetworkStatsAccess.Level.DEVICE);
+        assertTrue(requestByApp.requestId > 0);
+        assertTrue(Objects.equals(sTemplateWifi, requestByApp.template));
+        assertEquals(THRESHOLD_BYTES, requestByApp.thresholdInBytes);
+
+        // Verify the threshold requested by system uid won't be overridden.
+        final DataUsageRequest requestBySystem = mStatsObservers.register(inputRequest,
+                mUsageCallback, Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+        assertTrue(requestBySystem.requestId > 0);
+        assertTrue(Objects.equals(sTemplateWifi, requestBySystem.template));
+        assertEquals(1, requestBySystem.thresholdInBytes);
     }
 
     @Test
@@ -152,7 +147,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, highThresholdBytes);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
                 Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, request.template));
@@ -164,13 +159,13 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, THRESHOLD_BYTES);
 
-        DataUsageRequest request1 = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request1 = mStatsObservers.register(inputRequest, mUsageCallback,
                 Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request1.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, request1.template));
         assertEquals(THRESHOLD_BYTES, request1.thresholdInBytes);
 
-        DataUsageRequest request2 = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request2 = mStatsObservers.register(inputRequest, mUsageCallback,
                 Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request2.requestId > request1.requestId);
         assertTrue(Objects.equals(sTemplateWifi, request2.template));
@@ -190,17 +185,19 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
                 Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
-        Mockito.verify(mockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
+        Mockito.verify(mUsageCallbackBinder).linkToDeath(any(IBinder.DeathRecipient.class),
+                anyInt());
 
         mStatsObservers.unregister(request, Process.SYSTEM_UID);
         waitForObserverToIdle();
 
-        Mockito.verify(mockBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
+        Mockito.verify(mUsageCallbackBinder).unlinkToDeath(any(IBinder.DeathRecipient.class),
+                anyInt());
     }
 
     @Test
@@ -208,17 +205,18 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
                 UID_RED, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
         assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
-        Mockito.verify(mockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
+        Mockito.verify(mUsageCallbackBinder)
+                .linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
 
         mStatsObservers.unregister(request, UID_BLUE);
         waitForObserverToIdle();
 
-        Mockito.verifyZeroInteractions(mockBinder);
+        Mockito.verifyZeroInteractions(mUsageCallbackBinder);
     }
 
     private NetworkIdentitySet makeTestIdentSet() {
@@ -235,7 +233,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
                 Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -259,7 +257,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
                 Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -289,7 +287,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
                 Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -312,7 +310,7 @@
         mStatsObservers.updateStats(
                 xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
         waitForObserverToIdle();
-        assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType);
+        mUsageCallback.expectOnThresholdReached(request);
     }
 
     @Test
@@ -320,7 +318,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
                 UID_RED, NetworkStatsAccess.Level.DEFAULT);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -345,7 +343,7 @@
         mStatsObservers.updateStats(
                 xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
         waitForObserverToIdle();
-        assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType);
+        mUsageCallback.expectOnThresholdReached(request);
     }
 
     @Test
@@ -353,7 +351,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
                 UID_BLUE, NetworkStatsAccess.Level.DEFAULT);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -385,7 +383,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
                 UID_BLUE, NetworkStatsAccess.Level.USER);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -410,7 +408,7 @@
         mStatsObservers.updateStats(
                 xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
         waitForObserverToIdle();
-        assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType);
+        mUsageCallback.expectOnThresholdReached(request);
     }
 
     @Test
@@ -418,7 +416,7 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
 
-        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
                 UID_RED, NetworkStatsAccess.Level.USER);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -447,6 +445,5 @@
 
     private void waitForObserverToIdle() {
         HandlerUtils.waitForIdle(mObserverHandlerThread, WAIT_TIMEOUT_MS);
-        HandlerUtils.waitForIdle(mHandler, WAIT_TIMEOUT_MS);
     }
 }
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 40587c5..45f033c 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -66,21 +66,22 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.annotation.NonNull;
 import android.app.AlarmManager;
-import android.app.usage.NetworkStatsManager;
 import android.content.Context;
 import android.content.Intent;
 import android.database.ContentObserver;
@@ -101,13 +102,9 @@
 import android.net.netstats.provider.INetworkStatsProviderCallback;
 import android.net.wifi.WifiInfo;
 import android.os.Build;
-import android.os.ConditionVariable;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
-import android.os.Messenger;
 import android.os.PowerManager;
 import android.os.SimpleClock;
 import android.provider.Settings;
@@ -117,8 +114,11 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
-import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.LocationPermissionChecker;
+import com.android.net.module.util.Struct.U32;
+import com.android.net.module.util.Struct.U8;
 import com.android.server.net.NetworkStatsService.AlertObserver;
 import com.android.server.net.NetworkStatsService.NetworkStatsSettings;
 import com.android.server.net.NetworkStatsService.NetworkStatsSettings.Config;
@@ -188,11 +188,16 @@
     private @Mock TetheringManager mTetheringManager;
     private @Mock NetworkStatsFactory mStatsFactory;
     private @Mock NetworkStatsSettings mSettings;
-    private @Mock IBinder mBinder;
+    private @Mock IBinder mUsageCallbackBinder;
+    private TestableUsageCallback mUsageCallback;
     private @Mock AlarmManager mAlarmManager;
     @Mock
     private NetworkStatsSubscriptionsMonitor mNetworkStatsSubscriptionsMonitor;
+    private @Mock BpfInterfaceMapUpdater mBpfInterfaceMapUpdater;
     private HandlerThread mHandlerThread;
+    @Mock
+    private LocationPermissionChecker mLocationPermissionChecker;
+    private @Mock IBpfMap<U32, U8> mUidCounterSetMap;
 
     private NetworkStatsService mService;
     private INetworkStatsSession mSession;
@@ -262,7 +267,9 @@
         MockitoAnnotations.initMocks(this);
         final Context context = InstrumentationRegistry.getContext();
         mServiceContext = new MockContext(context);
-        when(sWifiInfo.getCurrentNetworkKey()).thenReturn(TEST_WIFI_NETWORK_KEY);
+        when(mLocationPermissionChecker.checkCallersLocationPermission(
+                any(), any(), anyInt(), anyBoolean(), any())).thenReturn(true);
+        when(sWifiInfo.getNetworkKey()).thenReturn(TEST_WIFI_NETWORK_KEY);
         mStatsDir = TestIoUtils.createTemporaryDirectory(getClass().getSimpleName());
 
         PowerManager powerManager = (PowerManager) mServiceContext.getSystemService(
@@ -309,6 +316,8 @@
         verify(mTetheringManager).registerTetheringEventCallback(
                 any(), tetheringEventCbCaptor.capture());
         mTetheringEventCallback = tetheringEventCbCaptor.getValue();
+
+        mUsageCallback = new TestableUsageCallback(mUsageCallbackBinder);
     }
 
     @NonNull
@@ -334,6 +343,21 @@
                 return mContentObserver = super.makeContentObserver(handler, settings, monitor);
             }
 
+            @Override
+            public LocationPermissionChecker makeLocationPermissionChecker(final Context context) {
+                return mLocationPermissionChecker;
+            }
+
+            @Override
+            public BpfInterfaceMapUpdater makeBpfInterfaceMapUpdater(
+                    @NonNull Context ctx, @NonNull Handler handler) {
+                return mBpfInterfaceMapUpdater;
+            }
+
+            @Override
+            public IBpfMap<U32, U8> getUidCounterSetMap() {
+                return mUidCounterSetMap;
+            }
         };
     }
 
@@ -459,8 +483,11 @@
                 .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, 0xFAAD, 256L, 2L, 128L, 1L, 0L)
                 .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 0L));
         mService.setUidForeground(UID_RED, false);
+        verify(mUidCounterSetMap, never()).deleteEntry(any());
         mService.incrementOperationCount(UID_RED, 0xFAAD, 4);
         mService.setUidForeground(UID_RED, true);
+        verify(mUidCounterSetMap).updateEntry(
+                eq(new U32(UID_RED)), eq(new U8((short) SET_FOREGROUND)));
         mService.incrementOperationCount(UID_RED, 0xFAAD, 6);
 
         forcePollAndWaitForIdle();
@@ -985,7 +1012,7 @@
     }
 
     @Test
-    public void testDetailedUidStats() throws Exception {
+    public void testUidStatsForTransport() throws Exception {
         // pretend that network comes online
         expectDefaultSettings();
         NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
@@ -1011,7 +1038,7 @@
                 .insertEntry(entry3));
         mService.incrementOperationCount(UID_RED, 0xF00D, 1);
 
-        NetworkStats stats = mService.getDetailedUidStats(INTERFACES_ALL);
+        NetworkStats stats = mService.getUidStatsForTransport(NetworkCapabilities.TRANSPORT_WIFI);
 
         assertEquals(3, stats.size());
         entry1.operations = 1;
@@ -1022,68 +1049,6 @@
     }
 
     @Test
-    public void testDetailedUidStats_Filtered() throws Exception {
-        // pretend that network comes online
-        expectDefaultSettings();
-
-        final String stackedIface = "stacked-test0";
-        final LinkProperties stackedProp = new LinkProperties();
-        stackedProp.setInterfaceName(stackedIface);
-        final NetworkStateSnapshot wifiState = buildWifiState();
-        wifiState.getLinkProperties().addStackedLink(stackedProp);
-        NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {wifiState};
-
-        expectNetworkStatsSummary(buildEmptyStats());
-        expectNetworkStatsUidDetail(buildEmptyStats());
-
-        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
-                new UnderlyingNetworkInfo[0]);
-
-        NetworkStats.Entry uidStats = new NetworkStats.Entry(
-                TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 1024L, 8L, 512L, 4L, 0L);
-        // Stacked on matching interface
-        NetworkStats.Entry tetheredStats1 = new NetworkStats.Entry(
-                stackedIface, UID_TETHERING, SET_DEFAULT, TAG_NONE, 1024L, 8L, 512L, 4L, 0L);
-        TetherStatsParcel tetherStatsParcel1 =
-                buildTetherStatsParcel(stackedIface, 1024L, 8L, 512L, 4L, 0);
-        // Different interface
-        TetherStatsParcel tetherStatsParcel2 =
-                buildTetherStatsParcel("otherif", 1024L, 8L, 512L, 4L, 0);
-
-        final String[] ifaceFilter = new String[] { TEST_IFACE };
-        final String[] augmentedIfaceFilter = new String[] { stackedIface, TEST_IFACE };
-        incrementCurrentTime(HOUR_IN_MILLIS);
-        expectDefaultSettings();
-        expectNetworkStatsSummary(buildEmptyStats());
-        when(mStatsFactory.augmentWithStackedInterfaces(eq(ifaceFilter)))
-                .thenReturn(augmentedIfaceFilter);
-        when(mStatsFactory.readNetworkStatsDetail(eq(UID_ALL), any(), eq(TAG_ALL)))
-                .thenReturn(new NetworkStats(getElapsedRealtime(), 1)
-                        .insertEntry(uidStats));
-        final TetherStatsParcel[] tetherStatsParcels =  {tetherStatsParcel1, tetherStatsParcel2};
-        when(mNetd.tetherGetStats()).thenReturn(tetherStatsParcels);
-
-        NetworkStats stats = mService.getDetailedUidStats(ifaceFilter);
-
-        // mStatsFactory#readNetworkStatsDetail() has the following invocations:
-        // 1) NetworkStatsService#systemReady from #setUp.
-        // 2) mService#notifyNetworkStatus in the test above.
-        //
-        // Additionally, we should have one call from the above call to mService#getDetailedUidStats
-        // with the augmented ifaceFilter.
-        verify(mStatsFactory, times(2)).readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL);
-        verify(mStatsFactory, times(1)).readNetworkStatsDetail(
-                eq(UID_ALL),
-                eq(augmentedIfaceFilter),
-                eq(TAG_ALL));
-        assertTrue(ArrayUtils.contains(stats.getUniqueIfaces(), TEST_IFACE));
-        assertTrue(ArrayUtils.contains(stats.getUniqueIfaces(), stackedIface));
-        assertEquals(2, stats.size());
-        assertEquals(uidStats, stats.getValues(0, null));
-        assertEquals(tetheredStats1, stats.getValues(1, null));
-    }
-
-    @Test
     public void testForegroundBackground() throws Exception {
         // pretend that network comes online
         expectDefaultSettings();
@@ -1119,6 +1084,8 @@
                 .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, 32L, 2L, 32L, 2L, 0L)
                 .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, 0xFAAD, 1L, 1L, 1L, 1L, 0L));
         mService.setUidForeground(UID_RED, true);
+        verify(mUidCounterSetMap).updateEntry(
+                eq(new U32(UID_RED)), eq(new U8((short) SET_FOREGROUND)));
         mService.incrementOperationCount(UID_RED, 0xFAAD, 1);
 
         forcePollAndWaitForIdle();
@@ -1294,20 +1261,14 @@
         DataUsageRequest inputRequest = new DataUsageRequest(
                 DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, thresholdInBytes);
 
-        // Create a messenger that waits for callback activity
-        ConditionVariable cv = new ConditionVariable(false);
-        LatchedHandler latchedHandler = new LatchedHandler(Looper.getMainLooper(), cv);
-        Messenger messenger = new Messenger(latchedHandler);
-
         // Force poll
         expectDefaultSettings();
         expectNetworkStatsSummary(buildEmptyStats());
         expectNetworkStatsUidDetail(buildEmptyStats());
 
         // Register and verify request and that binder was called
-        DataUsageRequest request =
-                mService.registerUsageCallback(mServiceContext.getOpPackageName(), inputRequest,
-                        messenger, mBinder);
+        DataUsageRequest request = mService.registerUsageCallback(
+                mServiceContext.getOpPackageName(), inputRequest, mUsageCallback);
         assertTrue(request.requestId > 0);
         assertTrue(Objects.equals(sTemplateWifi, request.template));
         long minThresholdInBytes = 2 * 1024 * 1024; // 2 MB
@@ -1316,7 +1277,7 @@
         HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT);
 
         // Make sure that the caller binder gets connected
-        verify(mBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
+        verify(mUsageCallbackBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
 
         // modify some number on wifi, and trigger poll event
         // not enough traffic to call data usage callback
@@ -1331,7 +1292,7 @@
         assertNetworkTotal(sTemplateWifi, 1024L, 1L, 2048L, 2L, 0);
 
         // make sure callback has not being called
-        assertEquals(INVALID_TYPE, latchedHandler.lastMessageType);
+        mUsageCallback.assertNoCallback();
 
         // and bump forward again, with counters going higher. this is
         // important, since it will trigger the data usage callback
@@ -1346,23 +1307,21 @@
         assertNetworkTotal(sTemplateWifi, 4096000L, 4L, 8192000L, 8L, 0);
 
 
-        // Wait for the caller to ack receipt of CALLBACK_LIMIT_REACHED
-        assertTrue(cv.block(WAIT_TIMEOUT));
-        assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, latchedHandler.lastMessageType);
-        cv.close();
+        // Wait for the caller to invoke expectOnThresholdReached.
+        mUsageCallback.expectOnThresholdReached(request);
 
         // Allow binder to disconnect
-        when(mBinder.unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt())).thenReturn(true);
+        when(mUsageCallbackBinder.unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt()))
+                .thenReturn(true);
 
         // Unregister request
         mService.unregisterUsageRequest(request);
 
-        // Wait for the caller to ack receipt of CALLBACK_RELEASED
-        assertTrue(cv.block(WAIT_TIMEOUT));
-        assertEquals(NetworkStatsManager.CALLBACK_RELEASED, latchedHandler.lastMessageType);
+        // Wait for the caller to invoke expectOnCallbackReleased.
+        mUsageCallback.expectOnCallbackReleased(request);
 
         // Make sure that the caller binder gets disconnected
-        verify(mBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
+        verify(mUsageCallbackBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
     }
 
     @Test
@@ -1688,6 +1647,28 @@
         provider.expectOnRequestStatsUpdate(0 /* unused */);
     }
 
+    /**
+     * Verify the service will throw exceptions if the template is location sensitive but
+     * the permission is not granted.
+     */
+    @Test
+    public void testEnforceTemplateLocationPermission() throws Exception {
+        when(mLocationPermissionChecker.checkCallersLocationPermission(
+                any(), any(), anyInt(), anyBoolean(), any())).thenReturn(false);
+        initWifiStats(buildWifiState(true, TEST_IFACE, IMSI_1));
+        assertThrows(SecurityException.class, () ->
+                assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0));
+        // Templates w/o wifi network keys can query stats as usual.
+        assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0);
+        assertNetworkTotal(sTemplateImsi1, 0L, 0L, 0L, 0L, 0);
+
+        when(mLocationPermissionChecker.checkCallersLocationPermission(
+                any(), any(), anyInt(), anyBoolean(), any())).thenReturn(true);
+        assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0);
+        assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0);
+        assertNetworkTotal(sTemplateImsi1, 0L, 0L, 0L, 0L, 0);
+    }
+
     private static File getBaseDir(File statsDir) {
         File baseDir = new File(statsDir, "netstats");
         baseDir.mkdirs();
@@ -1703,7 +1684,8 @@
     private void assertNetworkTotal(NetworkTemplate template, long start, long end, long rxBytes,
             long rxPackets, long txBytes, long txPackets, int operations) throws Exception {
         // verify history API
-        final NetworkStatsHistory history = mSession.getHistoryForNetwork(template, FIELD_ALL);
+        final NetworkStatsHistory history =
+                mSession.getHistoryIntervalForNetwork(template, FIELD_ALL, start, end);
         assertValues(history, start, end, rxBytes, rxPackets, txBytes, txPackets, operations);
 
         // verify summary API
@@ -1915,21 +1897,4 @@
     private void waitForIdle() {
         HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT);
     }
-
-    static class LatchedHandler extends Handler {
-        private final ConditionVariable mCv;
-        int lastMessageType = INVALID_TYPE;
-
-        LatchedHandler(Looper looper, ConditionVariable cv) {
-            super(looper);
-            mCv = cv;
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            lastMessageType = msg.what;
-            mCv.open();
-            super.handleMessage(msg);
-        }
-    }
 }
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java b/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
index 43aeec6..0d34609 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
@@ -35,8 +35,8 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.usage.NetworkStatsManager;
 import android.content.Context;
-import android.net.NetworkTemplate;
 import android.os.Build;
 import android.os.Looper;
 import android.os.Parcel;
@@ -282,7 +282,7 @@
         // NETWORK_TYPE_5G_NSA.
         setRatTypeForSub(TEST_SUBID1, TelephonyManager.NETWORK_TYPE_LTE,
                 OVERRIDE_NETWORK_TYPE_NR_NSA);
-        assertRatTypeChangedForSub(TEST_IMSI1, NetworkTemplate.NETWORK_TYPE_5G_NSA);
+        assertRatTypeChangedForSub(TEST_IMSI1, NetworkStatsManager.NETWORK_TYPE_5G_NSA);
         reset(mDelegate);
 
         // Set RAT type to LTE without NR connected, the RAT type should be downgraded to LTE.
diff --git a/tests/unit/java/com/android/server/net/TestableUsageCallback.kt b/tests/unit/java/com/android/server/net/TestableUsageCallback.kt
new file mode 100644
index 0000000..1917ec3
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/TestableUsageCallback.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2022 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.server.net
+
+import android.net.DataUsageRequest
+import android.net.netstats.IUsageCallback
+import android.os.IBinder
+import java.util.concurrent.LinkedBlockingQueue
+import java.util.concurrent.TimeUnit
+import kotlin.test.fail
+
+private const val DEFAULT_TIMEOUT_MS = 200L
+
+// TODO: Move the class to static libs once all downstream have IUsageCallback definition.
+class TestableUsageCallback(private val binder: IBinder) : IUsageCallback.Stub() {
+    sealed class CallbackType(val request: DataUsageRequest) {
+        class OnThresholdReached(request: DataUsageRequest) : CallbackType(request)
+        class OnCallbackReleased(request: DataUsageRequest) : CallbackType(request)
+    }
+
+    // TODO: Change to use ArrayTrackRecord once moved into to the module.
+    private val history = LinkedBlockingQueue<CallbackType>()
+
+    override fun onThresholdReached(request: DataUsageRequest) {
+        history.add(CallbackType.OnThresholdReached(request))
+    }
+
+    override fun onCallbackReleased(request: DataUsageRequest) {
+        history.add(CallbackType.OnCallbackReleased(request))
+    }
+
+    fun expectOnThresholdReached(request: DataUsageRequest) {
+        expectCallback<CallbackType.OnThresholdReached>(request, DEFAULT_TIMEOUT_MS)
+    }
+
+    fun expectOnCallbackReleased(request: DataUsageRequest) {
+        expectCallback<CallbackType.OnCallbackReleased>(request, DEFAULT_TIMEOUT_MS)
+    }
+
+    @JvmOverloads
+    fun assertNoCallback(timeout: Long = DEFAULT_TIMEOUT_MS) {
+        val cb = history.poll(timeout, TimeUnit.MILLISECONDS)
+        cb?.let { fail("Expected no callback but got $cb") }
+    }
+
+    // Expects a callback of the specified request on the specified network within the timeout.
+    // If no callback arrives, or a different callback arrives, fail.
+    private inline fun <reified T : CallbackType> expectCallback(
+        expectedRequest: DataUsageRequest,
+        timeoutMs: Long
+    ) {
+        history.poll(timeoutMs, TimeUnit.MILLISECONDS).let {
+            if (it !is T || it.request != expectedRequest) {
+                fail("Unexpected callback : $it," +
+                        " expected ${T::class} with Request[$expectedRequest]")
+            } else {
+                it
+            }
+        }
+    }
+
+    override fun asBinder(): IBinder {
+        return binder
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp
index fe971e7..04ba98f 100644
--- a/tests/unit/jni/Android.bp
+++ b/tests/unit/jni/Android.bp
@@ -24,7 +24,28 @@
         "libbpf_android",
         "liblog",
         "libnativehelper",
-        "libnetdbpf",
         "libnetdutils",
+        "libnetworkstats",
+    ],
+}
+
+cc_library_shared {
+    name: "libandroid_net_frameworktests_util_jni",
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+        "-Wthread-safety",
+    ],
+    srcs: [
+        "android_net_frameworktests_util/onload.cpp",
+    ],
+    static_libs: [
+        "libnet_utils_device_common_bpfjni",
+        "libtcutils",
+    ],
+    shared_libs: [
+        "liblog",
+        "libnativehelper",
     ],
 }
diff --git a/tests/unit/jni/android_net_frameworktests_util/onload.cpp b/tests/unit/jni/android_net_frameworktests_util/onload.cpp
new file mode 100644
index 0000000..06a3986
--- /dev/null
+++ b/tests/unit/jni/android_net_frameworktests_util/onload.cpp
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2022 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 <nativehelper/JNIHelp.h>
+#include "jni.h"
+
+#define LOG_TAG "NetFrameworkTestsJni"
+#include <android/log.h>
+
+namespace android {
+
+int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name);
+int register_com_android_net_module_util_TcUtils(JNIEnv* env, char const* class_name);
+
+extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
+    JNIEnv *env;
+    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+        __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, "ERROR: GetEnv failed");
+        return JNI_ERR;
+    }
+
+    if (register_com_android_net_module_util_BpfMap(env,
+            "android/net/frameworktests/util/BpfMap") < 0) return JNI_ERR;
+
+    if (register_com_android_net_module_util_TcUtils(env,
+            "android/net/frameworktests/util/TcUtils") < 0) return JNI_ERR;
+
+    return JNI_VERSION_1_6;
+}
+
+}; // namespace android
diff --git a/tests/unit/res/raw/netstats_uid_v16 b/tests/unit/res/raw/netstats_uid_v16
new file mode 100644
index 0000000..a6ee430
--- /dev/null
+++ b/tests/unit/res/raw/netstats_uid_v16
Binary files differ