Merge changes Ie1a36c3c,Id727cf7b,Ifba198d9 into main

* changes:
  Remove unnecessary check for skDestroyListener
  Remove TrafficController
  Remove native code path in BpfNetMaps.java
diff --git a/OWNERS b/OWNERS
index 649efda..b2176cc 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,3 +1,4 @@
+# Bug component: 31808
 set noparent
 file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking
 
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index c6468a0..c274165 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -23,7 +23,7 @@
 import android.content.Context;
 import android.net.INetd;
 import android.net.RoutingCoordinatorManager;
-import android.net.connectivity.TiramisuConnectivityInternalApiUtil;
+import android.net.connectivity.ConnectivityInternalApiUtil;
 import android.net.ip.IpServer;
 import android.os.Build;
 import android.os.Handler;
@@ -133,7 +133,7 @@
     public LateSdk<RoutingCoordinatorManager> getRoutingCoordinator(Context context) {
         if (!SdkLevel.isAtLeastS()) return new LateSdk<>(null);
         return new LateSdk<>(
-                TiramisuConnectivityInternalApiUtil.getRoutingCoordinatorManager(context));
+                ConnectivityInternalApiUtil.getRoutingCoordinatorManager(context));
     }
 
     /**
diff --git a/Tethering/src/com/android/networkstack/tethering/wear/CompanionDeviceManagerProxy.java b/Tethering/src/com/android/networkstack/tethering/wear/CompanionDeviceManagerProxy.java
index e94febb..e845f3f 100644
--- a/Tethering/src/com/android/networkstack/tethering/wear/CompanionDeviceManagerProxy.java
+++ b/Tethering/src/com/android/networkstack/tethering/wear/CompanionDeviceManagerProxy.java
@@ -19,7 +19,7 @@
 import android.companion.AssociationInfo;
 import android.companion.CompanionDeviceManager;
 import android.content.Context;
-import android.net.connectivity.TiramisuConnectivityInternalApiUtil;
+import android.net.connectivity.ConnectivityInternalApiUtil;
 import android.net.wear.ICompanionDeviceManagerProxy;
 import android.os.Build;
 import android.os.RemoteException;
@@ -39,7 +39,7 @@
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public CompanionDeviceManagerProxy(Context context) {
         mService = ICompanionDeviceManagerProxy.Stub.asInterface(
-                TiramisuConnectivityInternalApiUtil.getCompanionDeviceManagerProxyService(context));
+                ConnectivityInternalApiUtil.getCompanionDeviceManagerProxyService(context));
     }
 
     /**
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index 0702aa7..1c4a662 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -366,6 +366,11 @@
         private volatile Collection<TetheredClient> mClients = null;
         private volatile Network mUpstream = null;
 
+        // The dnsmasq in R might block netd for 20 seconds, which can also block tethering
+        // enable/disable for 20 seconds. To fix this, changing the timeouts from 5 seconds to 30
+        // seconds. See b/289881008.
+        private static final int EXPANDED_TIMEOUT_MS = 30000;
+
         MyTetheringEventCallback(TetheringManager tm, String iface) {
             this(tm, iface, null);
             mAcceptAnyUpstream = true;
@@ -424,13 +429,13 @@
         }
 
         public void awaitInterfaceTethered() throws Exception {
-            assertTrue("Ethernet not tethered after " + TIMEOUT_MS + "ms",
-                    mTetheringStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            assertTrue("Ethernet not tethered after " + EXPANDED_TIMEOUT_MS + "ms",
+                    mTetheringStartedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
 
         public void awaitInterfaceLocalOnly() throws Exception {
-            assertTrue("Ethernet not local-only after " + TIMEOUT_MS + "ms",
-                    mLocalOnlyStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            assertTrue("Ethernet not local-only after " + EXPANDED_TIMEOUT_MS + "ms",
+                    mLocalOnlyStartedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }
 
         // Used to check if the callback has registered. When the callback is registered,
@@ -444,8 +449,9 @@
         }
 
         public void awaitCallbackRegistered() throws Exception {
-            if (!mCallbackRegisteredLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
-                fail("Did not receive callback registered signal after " + TIMEOUT_MS + "ms");
+            if (!mCallbackRegisteredLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+                fail("Did not receive callback registered signal after " + EXPANDED_TIMEOUT_MS
+                        + "ms");
             }
         }
 
@@ -457,11 +463,11 @@
             if (!mInterfaceWasTethered && !mInterfaceWasLocalOnly) return;
 
             if (mInterfaceWasTethered) {
-                assertTrue(mIface + " not untethered after " + TIMEOUT_MS + "ms",
-                        mTetheringStoppedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+                assertTrue(mIface + " not untethered after " + EXPANDED_TIMEOUT_MS + "ms",
+                        mTetheringStoppedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS));
             } else if (mInterfaceWasLocalOnly) {
-                assertTrue(mIface + " not untethered after " + TIMEOUT_MS + "ms",
-                        mLocalOnlyStoppedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+                assertTrue(mIface + " not untethered after " + EXPANDED_TIMEOUT_MS + "ms",
+                        mLocalOnlyStoppedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS));
             } else {
                 fail(mIface + " cannot be both tethered and local-only. Update this test class.");
             }
@@ -488,8 +494,9 @@
         }
 
         public Collection<TetheredClient> awaitClientConnected() throws Exception {
-            assertTrue("Did not receive client connected callback after " + TIMEOUT_MS + "ms",
-                    mClientConnectedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            assertTrue("Did not receive client connected callback after "
+                    + EXPANDED_TIMEOUT_MS + "ms",
+                    mClientConnectedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS));
             return mClients;
         }
 
@@ -506,10 +513,10 @@
         }
 
         public Network awaitUpstreamChanged(boolean throwTimeoutException) throws Exception {
-            if (!mUpstreamLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+            if (!mUpstreamLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
                 final String errorMessage = "Did not receive upstream "
                             + (mAcceptAnyUpstream ? "any" : mExpectedUpstream)
-                            + " callback after " + TIMEOUT_MS + "ms";
+                            + " callback after " + EXPANDED_TIMEOUT_MS + "ms";
 
                 if (throwTimeoutException) {
                     throw new TimeoutException(errorMessage);
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 076fde3..4949eaa 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -47,7 +47,7 @@
 import android.util.Log;
 
 import androidx.annotation.NonNull;
-import androidx.test.filters.MediumTest;
+import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.net.module.util.Ipv6Utils;
@@ -79,7 +79,7 @@
 import java.util.concurrent.TimeoutException;
 
 @RunWith(AndroidJUnit4.class)
-@MediumTest
+@LargeTest
 public class EthernetTetheringTest extends EthernetTetheringTestBase {
     @Rule
     public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
diff --git a/common/flags.aconfig b/common/flags.aconfig
index 7235202..0c46b48 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -27,3 +27,10 @@
   description: "Set data saver through ConnectivityManager API"
   bug: "297836825"
 }
+
+flag {
+  name: "support_is_uid_networking_blocked"
+  namespace: "android_core_networking"
+  description: "This flag controls whether isUidNetworkingBlocked is supported"
+  bug: "297836825"
+}
diff --git a/common/src/com/android/net/module/util/bpf/IngressDiscardKey.java b/common/src/com/android/net/module/util/bpf/IngressDiscardKey.java
index eabcf3c..9fefb52 100644
--- a/common/src/com/android/net/module/util/bpf/IngressDiscardKey.java
+++ b/common/src/com/android/net/module/util/bpf/IngressDiscardKey.java
@@ -16,9 +16,12 @@
 
 package com.android.net.module.util.bpf;
 
+import com.android.net.module.util.InetAddressUtils;
 import com.android.net.module.util.Struct;
 
+import java.net.Inet4Address;
 import java.net.Inet6Address;
+import java.net.InetAddress;
 
 /** Key type for ingress discard map */
 public class IngressDiscardKey extends Struct {
@@ -29,4 +32,14 @@
     public IngressDiscardKey(final Inet6Address dstAddr) {
         this.dstAddr = dstAddr;
     }
+
+    private static Inet6Address getInet6Address(final InetAddress addr) {
+        return (addr instanceof Inet4Address)
+                ? InetAddressUtils.v4MappedV6Address((Inet4Address) addr)
+                : (Inet6Address) addr;
+    }
+
+    public IngressDiscardKey(final InetAddress dstAddr) {
+        this(getInet6Address(dstAddr));
+    }
 }
diff --git a/framework-t/api/system-lint-baseline.txt b/framework-t/api/system-lint-baseline.txt
index 9baf991..c6055f6 100644
--- a/framework-t/api/system-lint-baseline.txt
+++ b/framework-t/api/system-lint-baseline.txt
@@ -5,3 +5,149 @@
 
 GenericException: android.net.IpSecManager.IpSecTunnelInterface#finalize():
     Methods must not throw generic exceptions (`java.lang.Throwable`)
+
+
+UnflaggedApi: android.nearby.CredentialElement#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.nearby.CredentialElement.equals(Object)
+UnflaggedApi: android.nearby.CredentialElement#hashCode():
+    New API must be flagged with @FlaggedApi: method android.nearby.CredentialElement.hashCode()
+UnflaggedApi: android.nearby.DataElement#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.nearby.DataElement.equals(Object)
+UnflaggedApi: android.nearby.DataElement#hashCode():
+    New API must be flagged with @FlaggedApi: method android.nearby.DataElement.hashCode()
+UnflaggedApi: android.nearby.NearbyDevice#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.nearby.NearbyDevice.equals(Object)
+UnflaggedApi: android.nearby.NearbyDevice#hashCode():
+    New API must be flagged with @FlaggedApi: method android.nearby.NearbyDevice.hashCode()
+UnflaggedApi: android.nearby.NearbyDevice#toString():
+    New API must be flagged with @FlaggedApi: method android.nearby.NearbyDevice.toString()
+UnflaggedApi: android.nearby.OffloadCapability#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.nearby.OffloadCapability.equals(Object)
+UnflaggedApi: android.nearby.OffloadCapability#hashCode():
+    New API must be flagged with @FlaggedApi: method android.nearby.OffloadCapability.hashCode()
+UnflaggedApi: android.nearby.OffloadCapability#toString():
+    New API must be flagged with @FlaggedApi: method android.nearby.OffloadCapability.toString()
+UnflaggedApi: android.nearby.PresenceCredential#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.nearby.PresenceCredential.equals(Object)
+UnflaggedApi: android.nearby.PresenceCredential#hashCode():
+    New API must be flagged with @FlaggedApi: method android.nearby.PresenceCredential.hashCode()
+UnflaggedApi: android.nearby.PublicCredential#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.nearby.PublicCredential.equals(Object)
+UnflaggedApi: android.nearby.PublicCredential#hashCode():
+    New API must be flagged with @FlaggedApi: method android.nearby.PublicCredential.hashCode()
+UnflaggedApi: android.nearby.ScanRequest#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.nearby.ScanRequest.equals(Object)
+UnflaggedApi: android.nearby.ScanRequest#hashCode():
+    New API must be flagged with @FlaggedApi: method android.nearby.ScanRequest.hashCode()
+UnflaggedApi: android.nearby.ScanRequest#toString():
+    New API must be flagged with @FlaggedApi: method android.nearby.ScanRequest.toString()
+UnflaggedApi: android.net.EthernetNetworkManagementException#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.net.EthernetNetworkManagementException.equals(Object)
+UnflaggedApi: android.net.EthernetNetworkManagementException#hashCode():
+    New API must be flagged with @FlaggedApi: method android.net.EthernetNetworkManagementException.hashCode()
+UnflaggedApi: android.net.EthernetNetworkUpdateRequest#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.net.EthernetNetworkUpdateRequest.equals(Object)
+UnflaggedApi: android.net.EthernetNetworkUpdateRequest#hashCode():
+    New API must be flagged with @FlaggedApi: method android.net.EthernetNetworkUpdateRequest.hashCode()
+UnflaggedApi: android.net.EthernetNetworkUpdateRequest#toString():
+    New API must be flagged with @FlaggedApi: method android.net.EthernetNetworkUpdateRequest.toString()
+UnflaggedApi: android.net.IpSecManager.IpSecTunnelInterface#finalize():
+    New API must be flagged with @FlaggedApi: method android.net.IpSecManager.IpSecTunnelInterface.finalize()
+UnflaggedApi: android.net.IpSecManager.IpSecTunnelInterface#toString():
+    New API must be flagged with @FlaggedApi: method android.net.IpSecManager.IpSecTunnelInterface.toString()
+UnflaggedApi: android.net.IpSecTransform.Builder#buildTunnelModeTransform(java.net.InetAddress, android.net.IpSecManager.SecurityParameterIndex):
+    New API must be flagged with @FlaggedApi: method android.net.IpSecTransform.Builder.buildTunnelModeTransform(java.net.InetAddress,android.net.IpSecManager.SecurityParameterIndex)
+UnflaggedApi: android.net.NetworkStats.Entry#toString():
+    New API must be flagged with @FlaggedApi: method android.net.NetworkStats.Entry.toString()
+UnflaggedApi: android.net.nsd.NsdManager#registerOffloadEngine(String, long, long, java.util.concurrent.Executor, android.net.nsd.OffloadEngine):
+    New API must be flagged with @FlaggedApi: method android.net.nsd.NsdManager.registerOffloadEngine(String,long,long,java.util.concurrent.Executor,android.net.nsd.OffloadEngine)
+UnflaggedApi: android.net.nsd.NsdManager#unregisterOffloadEngine(android.net.nsd.OffloadEngine):
+    New API must be flagged with @FlaggedApi: method android.net.nsd.NsdManager.unregisterOffloadEngine(android.net.nsd.OffloadEngine)
+UnflaggedApi: android.net.nsd.OffloadEngine:
+    New API must be flagged with @FlaggedApi: class android.net.nsd.OffloadEngine
+UnflaggedApi: android.net.nsd.OffloadEngine#OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadEngine.OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK
+UnflaggedApi: android.net.nsd.OffloadEngine#OFFLOAD_TYPE_FILTER_QUERIES:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadEngine.OFFLOAD_TYPE_FILTER_QUERIES
+UnflaggedApi: android.net.nsd.OffloadEngine#OFFLOAD_TYPE_FILTER_REPLIES:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadEngine.OFFLOAD_TYPE_FILTER_REPLIES
+UnflaggedApi: android.net.nsd.OffloadEngine#OFFLOAD_TYPE_REPLY:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadEngine.OFFLOAD_TYPE_REPLY
+UnflaggedApi: android.net.nsd.OffloadEngine#onOffloadServiceRemoved(android.net.nsd.OffloadServiceInfo):
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadEngine.onOffloadServiceRemoved(android.net.nsd.OffloadServiceInfo)
+UnflaggedApi: android.net.nsd.OffloadEngine#onOffloadServiceUpdated(android.net.nsd.OffloadServiceInfo):
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadEngine.onOffloadServiceUpdated(android.net.nsd.OffloadServiceInfo)
+UnflaggedApi: android.net.nsd.OffloadServiceInfo:
+    New API must be flagged with @FlaggedApi: class android.net.nsd.OffloadServiceInfo
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#CONTENTS_FILE_DESCRIPTOR:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.CONTENTS_FILE_DESCRIPTOR
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#CREATOR:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.CREATOR
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#OffloadServiceInfo(android.net.nsd.OffloadServiceInfo.Key, java.util.List<java.lang.String>, String, byte[], int, long):
+    New API must be flagged with @FlaggedApi: constructor android.net.nsd.OffloadServiceInfo(android.net.nsd.OffloadServiceInfo.Key,java.util.List<java.lang.String>,String,byte[],int,long)
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#PARCELABLE_STABILITY_LOCAL:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.PARCELABLE_STABILITY_LOCAL
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#PARCELABLE_STABILITY_VINTF:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.PARCELABLE_STABILITY_VINTF
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#PARCELABLE_WRITE_RETURN_VALUE:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.PARCELABLE_WRITE_RETURN_VALUE
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#describeContents():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.describeContents()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.equals(Object)
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#getHostname():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.getHostname()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#getKey():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.getKey()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#getOffloadPayload():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.getOffloadPayload()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#getOffloadType():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.getOffloadType()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#getPriority():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.getPriority()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#getSubtypes():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.getSubtypes()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#hashCode():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.hashCode()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#toString():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.toString()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#writeToParcel(android.os.Parcel, int):
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.writeToParcel(android.os.Parcel,int)
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key:
+    New API must be flagged with @FlaggedApi: class android.net.nsd.OffloadServiceInfo.Key
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#CONTENTS_FILE_DESCRIPTOR:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.Key.CONTENTS_FILE_DESCRIPTOR
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#CREATOR:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.Key.CREATOR
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#Key(String, String):
+    New API must be flagged with @FlaggedApi: constructor android.net.nsd.OffloadServiceInfo.Key(String,String)
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#PARCELABLE_STABILITY_LOCAL:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.Key.PARCELABLE_STABILITY_LOCAL
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#PARCELABLE_STABILITY_VINTF:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.Key.PARCELABLE_STABILITY_VINTF
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#PARCELABLE_WRITE_RETURN_VALUE:
+    New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.Key.PARCELABLE_WRITE_RETURN_VALUE
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#describeContents():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.Key.describeContents()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#equals(Object):
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.Key.equals(Object)
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#getServiceName():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.Key.getServiceName()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#getServiceType():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.Key.getServiceType()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#hashCode():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.Key.hashCode()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#toString():
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.Key.toString()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#writeToParcel(android.os.Parcel, int):
+    New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.Key.writeToParcel(android.os.Parcel,int)
+UnflaggedApi: android.net.thread.ThreadNetworkController:
+    New API must be flagged with @FlaggedApi: class android.net.thread.ThreadNetworkController
+UnflaggedApi: android.net.thread.ThreadNetworkController#THREAD_VERSION_1_3:
+    New API must be flagged with @FlaggedApi: field android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3
+UnflaggedApi: android.net.thread.ThreadNetworkController#getThreadVersion():
+    New API must be flagged with @FlaggedApi: method android.net.thread.ThreadNetworkController.getThreadVersion()
+UnflaggedApi: android.net.thread.ThreadNetworkManager:
+    New API must be flagged with @FlaggedApi: class android.net.thread.ThreadNetworkManager
+UnflaggedApi: android.net.thread.ThreadNetworkManager#getAllThreadNetworkControllers():
+    New API must be flagged with @FlaggedApi: method android.net.thread.ThreadNetworkManager.getAllThreadNetworkControllers()
diff --git a/framework/Android.bp b/framework/Android.bp
index 103083f..c88bacc 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -266,6 +266,7 @@
         ":framework-connectivity-t-pre-jarjar{.jar}",
         ":framework-connectivity.stubs.module_lib{.jar}",
         ":framework-connectivity-t.stubs.module_lib{.jar}",
+        ":framework-connectivity-flagged-apis{.jar}",
         "jarjar-excludes.txt",
     ],
     tools: [
@@ -278,6 +279,7 @@
         "--prefix android.net.connectivity " +
         "--apistubs $(location :framework-connectivity.stubs.module_lib{.jar}) " +
         "--apistubs $(location :framework-connectivity-t.stubs.module_lib{.jar}) " +
+        "--apistubs $(location :framework-connectivity-flagged-apis{.jar}) " +
         // Make a ":"-separated list. There will be an extra ":" but empty items are ignored.
         "--unsupportedapi $$(printf ':%s' $(locations :connectivity-hiddenapi-files)) " +
         "--excludes $(location jarjar-excludes.txt) " +
@@ -289,6 +291,30 @@
     ],
 }
 
+droidstubs {
+    name: "framework-connectivity-flagged-apis-droidstubs",
+    srcs: [
+        ":framework-connectivity-sources",
+        ":framework-connectivity-tiramisu-updatable-sources",
+        ":framework-nearby-java-sources",
+        ":framework-thread-sources",
+    ],
+    flags: [
+        "--show-annotation android.annotation.FlaggedApi",
+    ],
+    aidl: {
+        include_dirs: [
+            "packages/modules/Connectivity/framework/aidl-export",
+            "frameworks/native/aidl/binder", // For PersistableBundle.aidl
+        ],
+    },
+}
+
+java_library {
+    name: "framework-connectivity-flagged-apis",
+    srcs: [":framework-connectivity-flagged-apis-droidstubs"],
+}
+
 // Library providing limited APIs within the connectivity module, so that R+ components like
 // Tethering have a controlled way to depend on newer components like framework-connectivity that
 // are not loaded on R.
@@ -306,7 +332,7 @@
         // Files listed here MUST all be annotated with @RequiresApi(Build.VERSION_CODES.S)
         // or above as appropriate so that API checks are enforced for R+ users of this library
         "src/android/net/RoutingCoordinatorManager.java",
-        "src/android/net/connectivity/TiramisuConnectivityInternalApiUtil.java",
+        "src/android/net/connectivity/ConnectivityInternalApiUtil.java",
     ],
     visibility: [
         "//packages/modules/Connectivity/Tethering:__subpackages__",
diff --git a/framework/aidl-export/android/net/LocalNetworkInfo.aidl b/framework/aidl-export/android/net/LocalNetworkInfo.aidl
new file mode 100644
index 0000000..fa0bc41
--- /dev/null
+++ b/framework/aidl-export/android/net/LocalNetworkInfo.aidl
@@ -0,0 +1,20 @@
+/**
+ *
+ * Copyright (C) 2023 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;
+
+parcelable LocalNetworkInfo;
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index 782e20a..4d55067 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -14,6 +14,7 @@
     method @NonNull public static android.util.Range<java.lang.Integer> getIpSecNetIdRange();
     method @Nullable @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public android.net.LinkProperties getRedactedLinkPropertiesForPackage(@NonNull android.net.LinkProperties, int, @NonNull String);
     method @Nullable @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public android.net.NetworkCapabilities getRedactedNetworkCapabilitiesForPackage(@NonNull android.net.NetworkCapabilities, int, @NonNull String);
+    method @FlaggedApi("com.android.net.flags.support_is_uid_networking_blocked") @RequiresPermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) public boolean isUidNetworkingBlocked(int, boolean);
     method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void registerDefaultNetworkCallbackForUid(int, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
     method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS}) public void registerSystemDefaultNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
     method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void removeUidFromMeteredNetworkAllowList(int);
diff --git a/framework/jarjar-excludes.txt b/framework/jarjar-excludes.txt
index bc3c8d1..09abd17 100644
--- a/framework/jarjar-excludes.txt
+++ b/framework/jarjar-excludes.txt
@@ -23,6 +23,7 @@
 # of these classes must be protected by a check for >= S SDK.
 # It's unlikely anybody else declares a hidden class with this name ?
 android\.net\.RoutingCoordinatorManager(\$.+)?
+android\.net\.LocalNetworkInfo(\$.+)?
 
 # KeepaliveUtils is used by ConnectivityManager CTS
 # TODO: move into service-connectivity so framework-connectivity stops using
@@ -37,9 +38,3 @@
 # This is required since android.net.http contains api classes and hidden classes.
 # TODO: Remove this after hidden classes are moved to different package
 android\.net\.http\..+
-
-# TODO: OffloadServiceInfo is being added as an API, but wasn't an API yet in the first module
-# versions targeting U. Do not jarjar it such versions so that tests do not have to cover both
-# cases. This will be removed in an upcoming change marking it as API.
-android\.net\.nsd\.OffloadServiceInfo(\$.+)?
-android\.net\.nsd\.OffloadEngine(\$.+)?
diff --git a/framework/src/android/net/BpfNetMapsConstants.java b/framework/src/android/net/BpfNetMapsConstants.java
index 8086809..c784597 100644
--- a/framework/src/android/net/BpfNetMapsConstants.java
+++ b/framework/src/android/net/BpfNetMapsConstants.java
@@ -54,6 +54,8 @@
             "/sys/fs/bpf/netd_shared/map_netd_cookie_tag_map";
     public static final String DATA_SAVER_ENABLED_MAP_PATH =
             "/sys/fs/bpf/netd_shared/map_netd_data_saver_enabled_map";
+    public static final String INGRESS_DISCARD_MAP_PATH =
+            "/sys/fs/bpf/netd_shared/map_netd_ingress_discard_map";
     public static final Struct.S32 UID_RULES_CONFIGURATION_KEY = new Struct.S32(0);
     public static final Struct.S32 CURRENT_STATS_MAP_CONFIGURATION_KEY = new Struct.S32(1);
     public static final Struct.S32 DATA_SAVER_ENABLED_KEY = new Struct.S32(0);
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index f44fd0e..66b2840 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -133,6 +133,8 @@
     public static class Flags {
         static final String SET_DATA_SAVER_VIA_CM =
                 "com.android.net.flags.set_data_saver_via_cm";
+        static final String SUPPORT_IS_UID_NETWORKING_BLOCKED =
+                "com.android.net.flags.support_is_uid_networking_blocked";
     }
 
     /**
@@ -3961,16 +3963,21 @@
          * @param network The {@link Network} of the satisfying network.
          * @param networkCapabilities The {@link NetworkCapabilities} of the satisfying network.
          * @param linkProperties The {@link LinkProperties} of the satisfying network.
+         * @param localInfo The {@link LocalNetworkInfo} of the satisfying network, or null
+         *                  if this network is not a local network.
          * @param blocked Whether access to the {@link Network} is blocked due to system policy.
          * @hide
          */
         public final void onAvailable(@NonNull Network network,
                 @NonNull NetworkCapabilities networkCapabilities,
-                @NonNull LinkProperties linkProperties, @BlockedReason int blocked) {
+                @NonNull LinkProperties linkProperties,
+                @Nullable LocalNetworkInfo localInfo,
+                @BlockedReason int blocked) {
             // Internally only this method is called when a new network is available, and
             // it calls the callback in the same way and order that older versions used
             // to call so as not to change the behavior.
             onAvailable(network, networkCapabilities, linkProperties, blocked != 0);
+            if (null != localInfo) onLocalNetworkInfoChanged(network, localInfo);
             onBlockedStatusChanged(network, blocked);
         }
 
@@ -3987,7 +3994,8 @@
          */
         public void onAvailable(@NonNull Network network,
                 @NonNull NetworkCapabilities networkCapabilities,
-                @NonNull LinkProperties linkProperties, boolean blocked) {
+                @NonNull LinkProperties linkProperties,
+                boolean blocked) {
             onAvailable(network);
             if (!networkCapabilities.hasCapability(
                     NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)) {
@@ -4114,6 +4122,19 @@
                 @NonNull LinkProperties linkProperties) {}
 
         /**
+         * Called when there is a change in the {@link LocalNetworkInfo} for this network.
+         *
+         * This is only called for local networks, that is those with the
+         * NET_CAPABILITY_LOCAL_NETWORK network capability.
+         *
+         * @param network the {@link Network} whose local network info has changed.
+         * @param localNetworkInfo the new {@link LocalNetworkInfo} for this network.
+         * @hide
+         */
+        public void onLocalNetworkInfoChanged(@NonNull Network network,
+                @NonNull LocalNetworkInfo localNetworkInfo) {}
+
+        /**
          * Called when the network the framework connected to for this request suspends data
          * transmission temporarily.
          *
@@ -4207,27 +4228,29 @@
     }
 
     /** @hide */
-    public static final int CALLBACK_PRECHECK            = 1;
+    public static final int CALLBACK_PRECHECK                   = 1;
     /** @hide */
-    public static final int CALLBACK_AVAILABLE           = 2;
+    public static final int CALLBACK_AVAILABLE                  = 2;
     /** @hide arg1 = TTL */
-    public static final int CALLBACK_LOSING              = 3;
+    public static final int CALLBACK_LOSING                     = 3;
     /** @hide */
-    public static final int CALLBACK_LOST                = 4;
+    public static final int CALLBACK_LOST                       = 4;
     /** @hide */
-    public static final int CALLBACK_UNAVAIL             = 5;
+    public static final int CALLBACK_UNAVAIL                    = 5;
     /** @hide */
-    public static final int CALLBACK_CAP_CHANGED         = 6;
+    public static final int CALLBACK_CAP_CHANGED                = 6;
     /** @hide */
-    public static final int CALLBACK_IP_CHANGED          = 7;
+    public static final int CALLBACK_IP_CHANGED                 = 7;
     /** @hide obj = NetworkCapabilities, arg1 = seq number */
-    private static final int EXPIRE_LEGACY_REQUEST       = 8;
+    private static final int EXPIRE_LEGACY_REQUEST              = 8;
     /** @hide */
-    public static final int CALLBACK_SUSPENDED           = 9;
+    public static final int CALLBACK_SUSPENDED                  = 9;
     /** @hide */
-    public static final int CALLBACK_RESUMED             = 10;
+    public static final int CALLBACK_RESUMED                    = 10;
     /** @hide */
-    public static final int CALLBACK_BLK_CHANGED         = 11;
+    public static final int CALLBACK_BLK_CHANGED                = 11;
+    /** @hide */
+    public static final int CALLBACK_LOCAL_NETWORK_INFO_CHANGED = 12;
 
     /** @hide */
     public static String getCallbackName(int whichCallback) {
@@ -4243,6 +4266,7 @@
             case CALLBACK_SUSPENDED:    return "CALLBACK_SUSPENDED";
             case CALLBACK_RESUMED:      return "CALLBACK_RESUMED";
             case CALLBACK_BLK_CHANGED:  return "CALLBACK_BLK_CHANGED";
+            case CALLBACK_LOCAL_NETWORK_INFO_CHANGED: return "CALLBACK_LOCAL_NETWORK_INFO_CHANGED";
             default:
                 return Integer.toString(whichCallback);
         }
@@ -4297,7 +4321,8 @@
                 case CALLBACK_AVAILABLE: {
                     NetworkCapabilities cap = getObject(message, NetworkCapabilities.class);
                     LinkProperties lp = getObject(message, LinkProperties.class);
-                    callback.onAvailable(network, cap, lp, message.arg1);
+                    LocalNetworkInfo lni = getObject(message, LocalNetworkInfo.class);
+                    callback.onAvailable(network, cap, lp, lni, message.arg1);
                     break;
                 }
                 case CALLBACK_LOSING: {
@@ -4322,6 +4347,11 @@
                     callback.onLinkPropertiesChanged(network, lp);
                     break;
                 }
+                case CALLBACK_LOCAL_NETWORK_INFO_CHANGED: {
+                    final LocalNetworkInfo info = getObject(message, LocalNetworkInfo.class);
+                    callback.onLocalNetworkInfoChanged(network, info);
+                    break;
+                }
                 case CALLBACK_SUSPENDED: {
                     callback.onNetworkSuspended(network);
                     break;
@@ -6311,8 +6341,8 @@
     // is provided by linux file group permission AID_NET_BW_ACCT and the
     // selinux context fs_bpf_net*.
     // Only the system server process and the network stack have access.
-    // TODO: Expose api when ready.
-    // @SystemApi(client = MODULE_LIBRARIES)
+    @FlaggedApi(Flags.SUPPORT_IS_UID_NETWORKING_BLOCKED)
+    @SystemApi(client = MODULE_LIBRARIES)
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)  // BPF maps were only mainlined in T
     @RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
     public boolean isUidNetworkingBlocked(int uid, boolean isNetworkMetered) {
diff --git a/framework/src/android/net/LocalNetworkInfo.java b/framework/src/android/net/LocalNetworkInfo.java
new file mode 100644
index 0000000..f945133
--- /dev/null
+++ b/framework/src/android/net/LocalNetworkInfo.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2023 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.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+
+/**
+ * Information about a local network.
+ *
+ * This is sent to ConnectivityManager.NetworkCallback.
+ * @hide
+ */
+// TODO : make public
+public final class LocalNetworkInfo implements Parcelable {
+    @Nullable private final Network mUpstreamNetwork;
+
+    public LocalNetworkInfo(@Nullable final Network upstreamNetwork) {
+        this.mUpstreamNetwork = upstreamNetwork;
+    }
+
+    /**
+     * Return the upstream network, or null if none.
+     */
+    @Nullable
+    public Network getUpstreamNetwork() {
+        return mUpstreamNetwork;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull final Parcel dest, final int flags) {
+        dest.writeParcelable(mUpstreamNetwork, flags);
+    }
+
+    @Override
+    public String toString() {
+        return "LocalNetworkInfo { upstream=" + mUpstreamNetwork + " }";
+    }
+
+    public static final @NonNull Creator<LocalNetworkInfo> CREATOR = new Creator<>() {
+        public LocalNetworkInfo createFromParcel(Parcel in) {
+            final Network upstreamNetwork = in.readParcelable(null);
+            return new LocalNetworkInfo(upstreamNetwork);
+        }
+
+        @Override
+        public LocalNetworkInfo[] newArray(final int size) {
+            return new LocalNetworkInfo[size];
+        }
+    };
+
+    /**
+     * Builder for LocalNetworkInfo
+     */
+    public static final class Builder {
+        @Nullable private Network mUpstreamNetwork;
+
+        /**
+         * Set the upstream network, or null if none.
+         * @return the builder
+         */
+        @NonNull public Builder setUpstreamNetwork(@Nullable final Network network) {
+            mUpstreamNetwork = network;
+            return this;
+        }
+
+        /**
+         * Build the LocalNetworkInfo
+         */
+        @NonNull public LocalNetworkInfo build() {
+            return new LocalNetworkInfo(mUpstreamNetwork);
+        }
+    }
+}
diff --git a/framework/src/android/net/connectivity/TiramisuConnectivityInternalApiUtil.java b/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
similarity index 94%
rename from framework/src/android/net/connectivity/TiramisuConnectivityInternalApiUtil.java
rename to framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
index c2d75d2..79f1f65 100644
--- a/framework/src/android/net/connectivity/TiramisuConnectivityInternalApiUtil.java
+++ b/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
@@ -35,9 +35,8 @@
  * linter).
  * @hide
  */
-// TODO : rename this so that it doesn't reference "Tiramisu" since it can be used in S.
 @RequiresApi(Build.VERSION_CODES.S)
-public class TiramisuConnectivityInternalApiUtil {
+public class ConnectivityInternalApiUtil {
 
     /**
      * Get a service binder token for
diff --git a/nearby/framework/Android.bp b/nearby/framework/Android.bp
index f329295..4bb9efd 100644
--- a/nearby/framework/Android.bp
+++ b/nearby/framework/Android.bp
@@ -26,6 +26,7 @@
     ],
     path: "java",
     visibility: [
+        "//packages/modules/Connectivity/framework:__subpackages__",
         "//packages/modules/Connectivity/framework-t:__subpackages__",
     ],
 }
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 3dec6cc..b7e928d 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -25,6 +25,7 @@
 import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_MAP_PATH;
 import static android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH;
 import static android.net.BpfNetMapsConstants.IIF_MATCH;
+import static android.net.BpfNetMapsConstants.INGRESS_DISCARD_MAP_PATH;
 import static android.net.BpfNetMapsConstants.LOCKDOWN_VPN_MATCH;
 import static android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH;
 import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
@@ -76,9 +77,12 @@
 import com.android.net.module.util.Struct.U8;
 import com.android.net.module.util.bpf.CookieTagMapKey;
 import com.android.net.module.util.bpf.CookieTagMapValue;
+import com.android.net.module.util.bpf.IngressDiscardKey;
+import com.android.net.module.util.bpf.IngressDiscardValue;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
+import java.net.InetAddress;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
@@ -126,6 +130,7 @@
     private static IBpfMap<CookieTagMapKey, CookieTagMapValue> sCookieTagMap = null;
     // TODO: Add BOOL class and replace U8?
     private static IBpfMap<S32, U8> sDataSaverEnabledMap = null;
+    private static IBpfMap<IngressDiscardKey, IngressDiscardValue> sIngressDiscardMap = null;
 
     private static final List<Pair<Integer, String>> PERMISSION_LIST = Arrays.asList(
             Pair.create(PERMISSION_INTERNET, "PERMISSION_INTERNET"),
@@ -173,6 +178,15 @@
         sDataSaverEnabledMap = dataSaverEnabledMap;
     }
 
+    /**
+     * Set ingressDiscardMap for test.
+     */
+    @VisibleForTesting
+    public static void setIngressDiscardMapForTest(
+            IBpfMap<IngressDiscardKey, IngressDiscardValue> ingressDiscardMap) {
+        sIngressDiscardMap = ingressDiscardMap;
+    }
+
     private static IBpfMap<S32, U32> getConfigurationMap() {
         try {
             return new BpfMap<>(
@@ -218,6 +232,15 @@
         }
     }
 
+    private static IBpfMap<IngressDiscardKey, IngressDiscardValue> getIngressDiscardMap() {
+        try {
+            return new BpfMap<>(INGRESS_DISCARD_MAP_PATH, BpfMap.BPF_F_RDWR,
+                    IngressDiscardKey.class, IngressDiscardValue.class);
+        } catch (ErrnoException e) {
+            throw new IllegalStateException("Cannot open ingress discard map", e);
+        }
+    }
+
     private static void initBpfMaps() {
         if (sConfigurationMap == null) {
             sConfigurationMap = getConfigurationMap();
@@ -260,6 +283,15 @@
         } catch (ErrnoException e) {
             throw new IllegalStateException("Failed to initialize data saver configuration", e);
         }
+
+        if (sIngressDiscardMap == null) {
+            sIngressDiscardMap = getIngressDiscardMap();
+        }
+        try {
+            sIngressDiscardMap.clear();
+        } catch (ErrnoException e) {
+            throw new IllegalStateException("Failed to initialize ingress discard map", e);
+        }
     }
 
     /**
@@ -285,6 +317,13 @@
         }
 
         /**
+         * Get interface name
+         */
+        public String getIfName(final int ifIndex) {
+            return Os.if_indextoname(ifIndex);
+        }
+
+        /**
          * Call synchronize_rcu()
          */
         public int synchronizeKernelRCU() {
@@ -497,7 +536,7 @@
 
     private Set<Integer> asSet(final int[] uids) {
         final Set<Integer> uidSet = new ArraySet<>();
-        for (final int uid: uids) {
+        for (final int uid : uids) {
             uidSet.add(uid);
         }
         return uidSet;
@@ -835,6 +874,45 @@
         }
     }
 
+    /**
+     * Set ingress discard rule
+     *
+     * @param address target address to set the ingress discard rule
+     * @param iface allowed interface
+     */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    public void setIngressDiscardRule(final InetAddress address, final String iface) {
+        throwIfPreT("setIngressDiscardRule is not available on pre-T devices");
+        final int ifIndex = mDeps.getIfIndex(iface);
+        if (ifIndex == 0) {
+            Log.e(TAG, "Failed to get if index, skip setting ingress discard rule for " + address
+                    + "(" + iface + ")");
+            return;
+        }
+        try {
+            sIngressDiscardMap.updateEntry(new IngressDiscardKey(address),
+                    new IngressDiscardValue(ifIndex, ifIndex));
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to set ingress discard rule for " + address + "("
+                    + iface + "), " + e);
+        }
+    }
+
+    /**
+     * Remove ingress discard rule
+     *
+     * @param address target address to remove the ingress discard rule
+     */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    public void removeIngressDiscardRule(final InetAddress address) {
+        throwIfPreT("removeIngressDiscardRule is not available on pre-T devices");
+        try {
+            sIngressDiscardMap.deleteEntry(new IngressDiscardKey(address));
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to remove ingress discard rule for " + address + ", " + e);
+        }
+    }
+
     /** Register callback for statsd to pull atom. */
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public void setPullAtomCallback(final Context context) {
@@ -975,7 +1053,10 @@
                     });
             BpfDump.dumpMap(sUidPermissionMap, pw, "sUidPermissionMap",
                     (uid, permission) -> uid.val + " " + permissionToString(permission.val));
-
+            BpfDump.dumpMap(sIngressDiscardMap, pw, "sIngressDiscardMap",
+                    (key, value) -> "[" + key.dstAddr + "]: "
+                            + value.iif1 + "(" + mDeps.getIfName(value.iif1) + "), "
+                            + value.iif2 + "(" + mDeps.getIfName(value.iif2) + ")");
             dumpDataSaverConfig(pw);
             pw.decreaseIndent();
         }
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 3dc5692..d8417c1 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -168,6 +168,7 @@
 import android.net.IpPrefix;
 import android.net.LinkProperties;
 import android.net.LocalNetworkConfig;
+import android.net.LocalNetworkInfo;
 import android.net.MatchAllNetworkSpecifier;
 import android.net.NativeNetworkConfig;
 import android.net.NativeNetworkType;
@@ -4183,7 +4184,7 @@
                 }
                 case NetworkAgent.EVENT_LOCAL_NETWORK_CONFIG_CHANGED: {
                     final LocalNetworkConfig config = (LocalNetworkConfig) arg.second;
-                    updateLocalNetworkConfig(nai, config);
+                    handleUpdateLocalNetworkConfig(nai, nai.localNetworkConfig, config);
                     break;
                 }
                 case NetworkAgent.EVENT_NETWORK_SCORE_CHANGED: {
@@ -4944,6 +4945,17 @@
             mDefaultInetConditionPublished = 0;
         }
         notifyIfacesChangedForNetworkStats();
+        // If this was a local network forwarded to some upstream, or if some local network was
+        // forwarded to this nai, then disable forwarding rules now.
+        maybeDisableForwardRulesForDisconnectingNai(nai, true /* sendCallbacks */);
+        // If this is a local network with an upstream selector, remove the associated network
+        // request.
+        if (nai.isLocalNetwork()) {
+            final NetworkRequest selector = nai.localNetworkConfig.getUpstreamSelector();
+            if (null != selector) {
+                handleRemoveNetworkRequest(mNetworkRequests.get(selector));
+            }
+        }
         // TODO - we shouldn't send CALLBACK_LOST to requests that can be satisfied
         // by other networks that are already connected. Perhaps that can be done by
         // sending all CALLBACK_LOST messages (for requests, not listens) at the end
@@ -5057,6 +5069,55 @@
         mNetIdManager.releaseNetId(nai.network.getNetId());
     }
 
+    private void maybeDisableForwardRulesForDisconnectingNai(
+            @NonNull final NetworkAgentInfo disconnecting, final boolean sendCallbacks) {
+        // Step 1 : maybe this network was the upstream for one or more local networks.
+        for (final NetworkAgentInfo local : mNetworkAgentInfos) {
+            if (!local.isLocalNetwork()) continue;
+            final NetworkRequest selector = local.localNetworkConfig.getUpstreamSelector();
+            if (null == selector) continue;
+            final NetworkRequestInfo nri = mNetworkRequests.get(selector);
+            // null == nri can happen while disconnecting a network, because destroyNetwork() is
+            // called after removing all associated NRIs from mNetworkRequests.
+            if (null == nri) continue;
+            final NetworkAgentInfo satisfier = nri.getSatisfier();
+            if (disconnecting != satisfier) continue;
+            removeLocalNetworkUpstream(local, disconnecting);
+            // Set the satisfier to null immediately so that the LOCAL_NETWORK_CHANGED callback
+            // correctly contains null as an upstream.
+            if (sendCallbacks) {
+                nri.setSatisfier(null, null);
+                notifyNetworkCallbacks(local,
+                        ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
+            }
+        }
+
+        // Step 2 : maybe this is a local network that had an upstream.
+        if (!disconnecting.isLocalNetwork()) return;
+        final NetworkRequest selector = disconnecting.localNetworkConfig.getUpstreamSelector();
+        if (null == selector) return;
+        final NetworkRequestInfo nri = mNetworkRequests.get(selector);
+        // As above null == nri can happen while disconnecting a network, because destroyNetwork()
+        // is called after removing all associated NRIs from mNetworkRequests.
+        if (null == nri) return;
+        final NetworkAgentInfo satisfier = nri.getSatisfier();
+        if (null == satisfier) return;
+        removeLocalNetworkUpstream(disconnecting, satisfier);
+    }
+
+    private void removeLocalNetworkUpstream(@NonNull final NetworkAgentInfo localAgent,
+            @NonNull final NetworkAgentInfo upstream) {
+        try {
+            mRoutingCoordinatorService.removeInterfaceForward(
+                    localAgent.linkProperties.getInterfaceName(),
+                    upstream.linkProperties.getInterfaceName());
+        } catch (RemoteException e) {
+            loge("Couldn't remove interface forward for "
+                    + localAgent.linkProperties.getInterfaceName() + " to "
+                    + upstream.linkProperties.getInterfaceName() + " while disconnecting");
+        }
+    }
+
     private boolean createNativeNetwork(@NonNull NetworkAgentInfo nai) {
         try {
             // This should never fail.  Specifying an already in use NetID will cause failure.
@@ -5071,10 +5132,9 @@
                         !nai.networkAgentConfig.allowBypass /* secure */,
                         getVpnType(nai), nai.networkAgentConfig.excludeLocalRouteVpn);
             } else {
-                final boolean hasLocalCap =
-                        nai.networkCapabilities.hasCapability(NET_CAPABILITY_LOCAL_NETWORK);
                 config = new NativeNetworkConfig(nai.network.getNetId(),
-                        hasLocalCap ? NativeNetworkType.PHYSICAL_LOCAL : NativeNetworkType.PHYSICAL,
+                        nai.isLocalNetwork() ? NativeNetworkType.PHYSICAL_LOCAL
+                                : NativeNetworkType.PHYSICAL,
                         getNetworkPermission(nai.networkCapabilities),
                         false /* secure */,
                         VpnManager.TYPE_VPN_NONE,
@@ -5095,6 +5155,11 @@
         if (mDscpPolicyTracker != null) {
             mDscpPolicyTracker.removeAllDscpPolicies(nai, false);
         }
+        // Remove any forwarding rules to and from the interface for this network, since
+        // the interface is going to go away. Don't send the callbacks however ; if the network
+        // was is being disconnected the callbacks have already been sent, and if it is being
+        // destroyed pending replacement they will be sent when it is disconnected.
+        maybeDisableForwardRulesForDisconnectingNai(nai, false /* sendCallbacks */);
         try {
             mNetd.networkDestroy(nai.network.getNetId());
         } catch (RemoteException | ServiceSpecificException e) {
@@ -8264,6 +8329,9 @@
             e.rethrowAsRuntimeException();
         }
 
+        if (nai.isLocalNetwork()) {
+            handleUpdateLocalNetworkConfig(nai, null /* oldConfig */, nai.localNetworkConfig);
+        }
         nai.notifyRegistered();
         NetworkInfo networkInfo = nai.networkInfo;
         updateNetworkInfo(nai, networkInfo);
@@ -8929,14 +8997,75 @@
         updateCapabilities(nai.getScore(), nai, nai.networkCapabilities);
     }
 
-    private void updateLocalNetworkConfig(@NonNull final NetworkAgentInfo nai,
-            @NonNull final LocalNetworkConfig config) {
-        if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)) {
+    // oldConfig is null iff this is the original registration of the local network config
+    private void handleUpdateLocalNetworkConfig(@NonNull final NetworkAgentInfo nai,
+            @Nullable final LocalNetworkConfig oldConfig,
+            @NonNull final LocalNetworkConfig newConfig) {
+        if (!nai.isLocalNetwork()) {
             Log.wtf(TAG, "Ignoring update of a local network info on non-local network " + nai);
             return;
         }
-        // TODO : actually apply the diff.
-        nai.localNetworkConfig = config;
+
+        final LocalNetworkConfig.Builder configBuilder = new LocalNetworkConfig.Builder();
+        // TODO : apply the diff for multicast routing.
+        configBuilder.setUpstreamMulticastRoutingConfig(
+                newConfig.getUpstreamMulticastRoutingConfig());
+        configBuilder.setDownstreamMulticastRoutingConfig(
+                newConfig.getDownstreamMulticastRoutingConfig());
+
+        final NetworkRequest oldRequest =
+                (null == oldConfig) ? null : oldConfig.getUpstreamSelector();
+        final NetworkCapabilities oldCaps =
+                (null == oldRequest) ? null : oldRequest.networkCapabilities;
+        final NetworkRequestInfo oldNri =
+                null == oldRequest ? null : mNetworkRequests.get(oldRequest);
+        final NetworkAgentInfo oldSatisfier =
+                null == oldNri ? null : oldNri.getSatisfier();
+        final NetworkRequest newRequest = newConfig.getUpstreamSelector();
+        final NetworkCapabilities newCaps =
+                (null == newRequest) ? null : newRequest.networkCapabilities;
+        final boolean requestUpdated = !Objects.equals(newCaps, oldCaps);
+        if (null != oldRequest && requestUpdated) {
+            handleRemoveNetworkRequest(mNetworkRequests.get(oldRequest));
+            if (null == newRequest && null != oldSatisfier) {
+                // If there is an old satisfier, but no new request, then remove the old upstream.
+                removeLocalNetworkUpstream(nai, oldSatisfier);
+                nai.localNetworkConfig = configBuilder.build();
+                // When there is a new request, the rematch sees the new request and sends the
+                // LOCAL_NETWORK_INFO_CHANGED callbacks accordingly.
+                // But here there is no new request, so the rematch won't see anything. Send
+                // callbacks to apps now to tell them about the loss of upstream.
+                notifyNetworkCallbacks(nai,
+                        ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
+                return;
+            }
+        }
+        if (null != newRequest && requestUpdated) {
+            // File the new request if :
+            //  - it has changed (requestUpdated), or
+            //  - it's the first time this local info (null == oldConfig)
+            // is updated and the request has not been filed yet.
+            // Requests for local info are always LISTEN_FOR_BEST, because they have at most one
+            // upstream (the best) but never request it to be brought up.
+            final NetworkRequest nr = new NetworkRequest(newCaps, ConnectivityManager.TYPE_NONE,
+                    nextNetworkRequestId(), LISTEN_FOR_BEST);
+            configBuilder.setUpstreamSelector(nr);
+            final NetworkRequestInfo nri = new NetworkRequestInfo(
+                    nai.creatorUid, nr, null /* messenger */, null /* binder */,
+                    0 /* callbackFlags */, null /* attributionTag */);
+            if (null != oldSatisfier) {
+                // Set the old satisfier in the new NRI so that the rematch will see any changes
+                nri.setSatisfier(oldSatisfier, nr);
+            }
+            nai.localNetworkConfig = configBuilder.build();
+            // handleRegisterNetworkRequest causes a rematch. The rematch must happen after
+            // nai.localNetworkConfig is set, since it will base its callbacks on the old
+            // satisfier and the new request.
+            handleRegisterNetworkRequest(nri);
+        } else {
+            configBuilder.setUpstreamSelector(oldRequest);
+            nai.localNetworkConfig = configBuilder.build();
+        }
     }
 
     /**
@@ -9267,6 +9396,21 @@
         releasePendingNetworkRequestWithDelay(pendingIntent);
     }
 
+    @Nullable
+    private LocalNetworkInfo localNetworkInfoForNai(@NonNull final NetworkAgentInfo nai) {
+        if (!nai.isLocalNetwork()) return null;
+        final Network upstream;
+        final NetworkRequest selector = nai.localNetworkConfig.getUpstreamSelector();
+        if (null == selector) {
+            upstream = null;
+        } else {
+            final NetworkRequestInfo upstreamNri = mNetworkRequests.get(selector);
+            final NetworkAgentInfo satisfier = upstreamNri.getSatisfier();
+            upstream = (null == satisfier) ? null : satisfier.network;
+        }
+        return new LocalNetworkInfo.Builder().setUpstreamNetwork(upstream).build();
+    }
+
     // networkAgent is only allowed to be null if notificationType is
     // CALLBACK_UNAVAIL. This is because UNAVAIL is about no network being
     // available, while all other cases are about some particular network.
@@ -9302,6 +9446,10 @@
                 putParcelable(bundle, nc);
                 putParcelable(bundle, linkPropertiesRestrictedForCallerPermissions(
                         networkAgent.linkProperties, nri.mPid, nri.mUid));
+                // The local network info is often null, so can't use the static putParcelable
+                // method here.
+                bundle.putParcelable(LocalNetworkInfo.class.getSimpleName(),
+                        localNetworkInfoForNai(networkAgent));
                 // For this notification, arg1 contains the blocked status.
                 msg.arg1 = arg1;
                 break;
@@ -9333,6 +9481,14 @@
                 msg.arg1 = arg1;
                 break;
             }
+            case ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED: {
+                if (!networkAgent.isLocalNetwork()) {
+                    Log.wtf(TAG, "Callback for local info for a non-local network");
+                    return;
+                }
+                putParcelable(bundle, localNetworkInfoForNai(networkAgent));
+                break;
+            }
         }
         msg.what = notificationType;
         msg.setData(bundle);
@@ -9718,7 +9874,8 @@
             if (VDBG) log("rematch for " + newSatisfier.toShortString());
             if (null != previousRequest && null != previousSatisfier) {
                 if (VDBG || DDBG) {
-                    log("   accepting network in place of " + previousSatisfier.toShortString());
+                    log("   accepting network in place of " + previousSatisfier.toShortString()
+                            + " for " + newRequest);
                 }
                 previousSatisfier.removeRequest(previousRequest.requestId);
                 if (canSupportGracefulNetworkSwitch(previousSatisfier, newSatisfier)
@@ -9737,7 +9894,7 @@
                     previousSatisfier.lingerRequest(previousRequest.requestId, now);
                 }
             } else {
-                if (VDBG || DDBG) log("   accepting network in place of null");
+                if (VDBG || DDBG) log("   accepting network in place of null for " + newRequest);
             }
 
             // To prevent constantly CPU wake up for nascent timer, if a network comes up
@@ -9853,6 +10010,14 @@
         }
     }
 
+    private boolean hasSameInterfaceName(@Nullable final NetworkAgentInfo nai1,
+            @Nullable final NetworkAgentInfo nai2) {
+        if (null == nai1) return null == nai2;
+        if (null == nai2) return false;
+        return nai1.linkProperties.getInterfaceName()
+                .equals(nai2.linkProperties.getInterfaceName());
+    }
+
     private void applyNetworkReassignment(@NonNull final NetworkReassignment changes,
             final long now) {
         final Collection<NetworkAgentInfo> nais = mNetworkAgentInfos;
@@ -9926,6 +10091,40 @@
             notifyNetworkLosing(nai, now);
         }
 
+        // Update forwarding rules for the upstreams of local networks. Do this after sending
+        // onAvailable so that clients understand what network this is about.
+        for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+            if (!nai.isLocalNetwork()) continue;
+            final NetworkRequest nr = nai.localNetworkConfig.getUpstreamSelector();
+            if (null == nr) continue; // No upstream for this local network
+            final NetworkRequestInfo nri = mNetworkRequests.get(nr);
+            final NetworkReassignment.RequestReassignment change = changes.getReassignment(nri);
+            if (null == change) continue; // No change in upstreams for this network
+            final String fromIface = nai.linkProperties.getInterfaceName();
+            if (!hasSameInterfaceName(change.mOldNetwork, change.mNewNetwork)
+                    || change.mOldNetwork.isDestroyed()) {
+                // There can be a change with the same interface name if the new network is the
+                // replacement for the old network that was unregisteredAfterReplacement.
+                try {
+                    if (null != change.mOldNetwork) {
+                        mRoutingCoordinatorService.removeInterfaceForward(fromIface,
+                                change.mOldNetwork.linkProperties.getInterfaceName());
+                    }
+                    // If the new upstream is already destroyed, there is no point in setting up
+                    // a forward (in fact, it might forward to the interface for some new network !)
+                    // Later when the upstream disconnects CS will try to remove the forward, which
+                    // is ignored with a benign log by RoutingCoordinatorService.
+                    if (null != change.mNewNetwork && !change.mNewNetwork.isDestroyed()) {
+                        mRoutingCoordinatorService.addInterfaceForward(fromIface,
+                                change.mNewNetwork.linkProperties.getInterfaceName());
+                    }
+                } catch (final RemoteException e) {
+                    loge("Can't update forwarding rules", e);
+                }
+            }
+            notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
+        }
+
         updateLegacyTypeTrackerAndVpnLockdownForRematch(changes, nais);
 
         // Tear down all unneeded networks.
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index dacae20..7cd3cc8 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -1258,6 +1258,11 @@
         return networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
     }
 
+    /** Whether this network is a local network */
+    public boolean isLocalNetwork() {
+        return networkCapabilities.hasCapability(NET_CAPABILITY_LOCAL_NETWORK);
+    }
+
     /**
      * Whether this network should propagate the capabilities from its underlying networks.
      * Currently only true for VPNs.
diff --git a/service/src/com/android/server/connectivity/RoutingCoordinatorService.java b/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
index ede78ce..3350d2d 100644
--- a/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
+++ b/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
@@ -197,8 +197,16 @@
         synchronized (mIfacesLock) {
             final ForwardingPair fwp = new ForwardingPair(fromIface, toIface);
             if (!mForwardedInterfaces.contains(fwp)) {
-                throw new IllegalStateException("No forward set up between interfaces "
-                        + fromIface + " → " + toIface);
+                // This can happen when an upstream was unregisteredAfterReplacement. The forward
+                // is removed immediately when the upstream is destroyed, but later when the
+                // network actually disconnects CS does not know that and it asks for removal
+                // again.
+                // This can also happen if the network was destroyed before being set as an
+                // upstream, because then CS does not set up the forward rules seeing how the
+                // interface was removed anyway.
+                // Either way, this is benign.
+                Log.i(TAG, "No forward set up between interfaces " + fromIface + " → " + toIface);
+                return;
             }
             mForwardedInterfaces.remove(fwp);
             try {
diff --git a/staticlibs/device/com/android/net/module/util/FeatureVersions.java b/staticlibs/device/com/android/net/module/util/FeatureVersions.java
index 149756c..d5f8124 100644
--- a/staticlibs/device/com/android/net/module/util/FeatureVersions.java
+++ b/staticlibs/device/com/android/net/module/util/FeatureVersions.java
@@ -42,4 +42,10 @@
     // M-2023-Sept on July 3rd, 2023.
     public static final long FEATURE_CLAT_ADDRESS_TRANSLATE =
             NETWORK_STACK_MODULE_ID + 34_09_00_000L;
+
+    // IS_UID_NETWORKING_BLOCKED is a feature in ConnectivityManager,
+    // which provides an API to access BPF maps to check whether the networking is blocked
+    // by BPF for the given uid and conditions, introduced in version M-2024-Feb on Nov 6, 2023.
+    public static final long FEATURE_IS_UID_NETWORKING_BLOCKED =
+            CONNECTIVITY_MODULE_ID + 34_14_00_000L;
 }
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
index df9c61a..05c0444 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
@@ -18,6 +18,7 @@
 
 import android.net.ConnectivityManager.NetworkCallback
 import android.net.LinkProperties
+import android.net.LocalNetworkInfo
 import android.net.Network
 import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
@@ -28,6 +29,7 @@
 import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatusInt
 import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
 import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.LocalInfoChanged
 import com.android.testutils.RecorderCallback.CallbackEntry.Losing
 import com.android.testutils.RecorderCallback.CallbackEntry.Lost
 import com.android.testutils.RecorderCallback.CallbackEntry.Resumed
@@ -68,6 +70,10 @@
             override val network: Network,
             val lp: LinkProperties
         ) : CallbackEntry()
+        data class LocalInfoChanged(
+            override val network: Network,
+            val info: LocalNetworkInfo
+        ) : CallbackEntry()
         data class Suspended(override val network: Network) : CallbackEntry()
         data class Resumed(override val network: Network) : CallbackEntry()
         data class Losing(override val network: Network, val maxMsToLive: Int) : CallbackEntry()
@@ -94,6 +100,8 @@
             @JvmField
             val LINK_PROPERTIES_CHANGED = LinkPropertiesChanged::class
             @JvmField
+            val LOCAL_INFO_CHANGED = LocalInfoChanged::class
+            @JvmField
             val SUSPENDED = Suspended::class
             @JvmField
             val RESUMED = Resumed::class
@@ -131,6 +139,11 @@
         history.add(LinkPropertiesChanged(network, lp))
     }
 
+    override fun onLocalNetworkInfoChanged(network: Network, info: LocalNetworkInfo) {
+        Log.d(TAG, "onLocalNetworkInfoChanged $network $info")
+        history.add(LocalInfoChanged(network, info))
+    }
+
     override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
         Log.d(TAG, "onBlockedStatusChanged $network $blocked")
         history.add(BlockedStatus(network, blocked))
@@ -430,37 +443,63 @@
         suspended: Boolean = false,
         validated: Boolean? = true,
         blocked: Boolean = false,
+        upstream: Network? = null,
         tmt: Long = defaultTimeoutMs
     ) {
-        expectAvailableCallbacksCommon(net, suspended, validated, tmt)
+        expectAvailableCallbacksCommon(net, suspended, validated, upstream, tmt)
         expect<BlockedStatus>(net, tmt) { it.blocked == blocked }
     }
 
+    // For backward compatibility, add a method that allows callers to specify a timeout but
+    // no upstream.
+    fun expectAvailableCallbacks(
+        net: Network,
+        suspended: Boolean = false,
+        validated: Boolean? = true,
+        blocked: Boolean = false,
+        tmt: Long = defaultTimeoutMs
+    ) = expectAvailableCallbacks(net, suspended, validated, blocked, upstream = null, tmt = tmt)
+
     fun expectAvailableCallbacks(
         net: Network,
         suspended: Boolean,
         validated: Boolean,
         blockedReason: Int,
+        upstream: Network? = null,
         tmt: Long
     ) {
-        expectAvailableCallbacksCommon(net, suspended, validated, tmt)
+        expectAvailableCallbacksCommon(net, suspended, validated, upstream, tmt)
         expect<BlockedStatusInt>(net) { it.reason == blockedReason }
     }
 
+    // For backward compatibility, add a method that allows callers to specify a timeout but
+    // no upstream.
+    fun expectAvailableCallbacks(
+            net: Network,
+            suspended: Boolean = false,
+            validated: Boolean = true,
+            blockedReason: Int,
+            tmt: Long = defaultTimeoutMs
+    ) = expectAvailableCallbacks(net, suspended, validated, blockedReason, upstream = null, tmt)
+
     private fun expectAvailableCallbacksCommon(
         net: Network,
         suspended: Boolean,
         validated: Boolean?,
+        upstream: Network?,
         tmt: Long
     ) {
         expect<Available>(net, tmt)
         if (suspended) {
             expect<Suspended>(net, tmt)
         }
-        expect<CapabilitiesChanged>(net, tmt) {
+        val caps = expect<CapabilitiesChanged>(net, tmt) {
             validated == null || validated == it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
-        }
+        }.caps
         expect<LinkPropertiesChanged>(net, tmt)
+        if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)) {
+            expect<LocalInfoChanged>(net, tmt) { it.info.upstreamNetwork == upstream }
+        }
     }
 
     // Backward compatibility for existing Java code. Use named arguments instead and remove all
@@ -507,13 +546,15 @@
         val network: Network
     }
 
+    @JvmOverloads
     fun expectAvailableCallbacks(
         n: HasNetwork,
         suspended: Boolean,
         validated: Boolean,
         blocked: Boolean,
+        upstream: Network? = null,
         timeoutMs: Long
-    ) = expectAvailableCallbacks(n.network, suspended, validated, blocked, timeoutMs)
+    ) = expectAvailableCallbacks(n.network, suspended, validated, blocked, upstream, timeoutMs)
 
     fun expectAvailableAndSuspendedCallbacks(n: HasNetwork, expectValidated: Boolean) {
         expectAvailableAndSuspendedCallbacks(n.network, expectValidated)
diff --git a/tests/common/java/android/net/CaptivePortalDataTest.kt b/tests/common/java/android/net/CaptivePortalDataTest.kt
index f927380..67a523c 100644
--- a/tests/common/java/android/net/CaptivePortalDataTest.kt
+++ b/tests/common/java/android/net/CaptivePortalDataTest.kt
@@ -19,21 +19,20 @@
 import android.os.Build
 import androidx.test.filters.SmallTest
 import com.android.modules.utils.build.SdkLevel
-import com.android.testutils.assertParcelingIsLossless
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.assertParcelingIsLossless
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import kotlin.test.assertEquals
-import kotlin.test.assertNotEquals
 
 @SmallTest
 @RunWith(DevSdkIgnoreRunner::class)
-@IgnoreUpTo(Build.VERSION_CODES.Q)
 class CaptivePortalDataTest {
     @Rule @JvmField
     val ignoreRule = DevSdkIgnoreRule()
diff --git a/tests/common/java/android/net/KeepalivePacketDataTest.kt b/tests/common/java/android/net/KeepalivePacketDataTest.kt
index 403d6b5..97a45fc 100644
--- a/tests/common/java/android/net/KeepalivePacketDataTest.kt
+++ b/tests/common/java/android/net/KeepalivePacketDataTest.kt
@@ -17,27 +17,20 @@
 
 import android.net.InvalidPacketException.ERROR_INVALID_IP_ADDRESS
 import android.net.InvalidPacketException.ERROR_INVALID_PORT
-import android.os.Build
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.NonNullTestUtils
 import java.net.InetAddress
 import java.util.Arrays
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class KeepalivePacketDataTest {
-    @Rule @JvmField
-    val ignoreRule: DevSdkIgnoreRule = DevSdkIgnoreRule()
-
     private val INVALID_PORT = 65537
     private val TEST_DST_PORT = 4244
     private val TEST_SRC_PORT = 4243
@@ -60,7 +53,6 @@
             NonNullTestUtils.nullUnsafe(dstAddress), dstPort, data)
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     fun testConstructor() {
         try {
             TestKeepalivePacketData(srcAddress = null)
@@ -99,22 +91,17 @@
     }
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     fun testSrcAddress() = assertEquals(TEST_SRC_ADDRV4, TestKeepalivePacketData().srcAddress)
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     fun testDstAddress() = assertEquals(TEST_DST_ADDRV4, TestKeepalivePacketData().dstAddress)
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     fun testSrcPort() = assertEquals(TEST_SRC_PORT, TestKeepalivePacketData().srcPort)
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     fun testDstPort() = assertEquals(TEST_DST_PORT, TestKeepalivePacketData().dstPort)
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     fun testPacket() = assertTrue(Arrays.equals(TESTBYTES, TestKeepalivePacketData().packet))
 }
diff --git a/tests/common/java/android/net/LinkPropertiesTest.java b/tests/common/java/android/net/LinkPropertiesTest.java
index d2e7c99..8f14572 100644
--- a/tests/common/java/android/net/LinkPropertiesTest.java
+++ b/tests/common/java/android/net/LinkPropertiesTest.java
@@ -134,13 +134,10 @@
         assertFalse(lp.isIpv4Provisioned());
         assertFalse(lp.isIpv6Provisioned());
         assertFalse(lp.isPrivateDnsActive());
-
-        if (SdkLevel.isAtLeastR()) {
-            assertNull(lp.getDhcpServerAddress());
-            assertFalse(lp.isWakeOnLanSupported());
-            assertNull(lp.getCaptivePortalApiUrl());
-            assertNull(lp.getCaptivePortalData());
-        }
+        assertNull(lp.getDhcpServerAddress());
+        assertFalse(lp.isWakeOnLanSupported());
+        assertNull(lp.getCaptivePortalApiUrl());
+        assertNull(lp.getCaptivePortalData());
     }
 
     private LinkProperties makeTestObject() {
@@ -162,12 +159,10 @@
         lp.setMtu(MTU);
         lp.setTcpBufferSizes(TCP_BUFFER_SIZES);
         lp.setNat64Prefix(new IpPrefix("2001:db8:0:64::/96"));
-        if (SdkLevel.isAtLeastR()) {
-            lp.setDhcpServerAddress(DHCPSERVER);
-            lp.setWakeOnLanSupported(true);
-            lp.setCaptivePortalApiUrl(CAPPORT_API_URL);
-            lp.setCaptivePortalData((CaptivePortalData) getCaptivePortalData());
-        }
+        lp.setDhcpServerAddress(DHCPSERVER);
+        lp.setWakeOnLanSupported(true);
+        lp.setCaptivePortalApiUrl(CAPPORT_API_URL);
+        lp.setCaptivePortalData((CaptivePortalData) getCaptivePortalData());
         return lp;
     }
 
@@ -206,19 +201,17 @@
         assertTrue(source.isIdenticalTcpBufferSizes(target));
         assertTrue(target.isIdenticalTcpBufferSizes(source));
 
-        if (SdkLevel.isAtLeastR()) {
-            assertTrue(source.isIdenticalDhcpServerAddress(target));
-            assertTrue(source.isIdenticalDhcpServerAddress(source));
+        assertTrue(source.isIdenticalDhcpServerAddress(target));
+        assertTrue(source.isIdenticalDhcpServerAddress(source));
 
-            assertTrue(source.isIdenticalWakeOnLan(target));
-            assertTrue(target.isIdenticalWakeOnLan(source));
+        assertTrue(source.isIdenticalWakeOnLan(target));
+        assertTrue(target.isIdenticalWakeOnLan(source));
 
-            assertTrue(source.isIdenticalCaptivePortalApiUrl(target));
-            assertTrue(target.isIdenticalCaptivePortalApiUrl(source));
+        assertTrue(source.isIdenticalCaptivePortalApiUrl(target));
+        assertTrue(target.isIdenticalCaptivePortalApiUrl(source));
 
-            assertTrue(source.isIdenticalCaptivePortalData(target));
-            assertTrue(target.isIdenticalCaptivePortalData(source));
-        }
+        assertTrue(source.isIdenticalCaptivePortalData(target));
+        assertTrue(target.isIdenticalCaptivePortalData(source));
 
         // Check result of equals().
         assertTrue(source.equals(target));
@@ -1017,7 +1010,7 @@
         assertParcelingIsLossless(source);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testLinkPropertiesParcelable() throws Exception {
         final LinkProperties source = makeLinkPropertiesForParceling();
 
@@ -1035,7 +1028,7 @@
     }
 
     // Parceling of the scope was broken until Q-QPR2
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testLinkLocalDnsServerParceling() throws Exception {
         final String strAddress = "fe80::1%lo";
         final LinkProperties lp = new LinkProperties();
@@ -1158,7 +1151,7 @@
         assertFalse(lp.isPrivateDnsActive());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testDhcpServerAddress() {
         final LinkProperties lp = makeTestObject();
         assertEquals(DHCPSERVER, lp.getDhcpServerAddress());
@@ -1167,7 +1160,7 @@
         assertNull(lp.getDhcpServerAddress());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testWakeOnLanSupported() {
         final LinkProperties lp = makeTestObject();
         assertTrue(lp.isWakeOnLanSupported());
@@ -1176,7 +1169,7 @@
         assertFalse(lp.isWakeOnLanSupported());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testCaptivePortalApiUrl() {
         final LinkProperties lp = makeTestObject();
         assertEquals(CAPPORT_API_URL, lp.getCaptivePortalApiUrl());
@@ -1185,7 +1178,7 @@
         assertNull(lp.getCaptivePortalApiUrl());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testCaptivePortalData() {
         final LinkProperties lp = makeTestObject();
         assertEquals(getCaptivePortalData(), lp.getCaptivePortalData());
@@ -1238,7 +1231,7 @@
         assertTrue(Ipv6.hasIpv6DnsServer());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testHasIpv4UnreachableDefaultRoute() {
         final LinkProperties lp = makeTestObject();
         assertFalse(lp.hasIpv4UnreachableDefaultRoute());
@@ -1249,7 +1242,7 @@
         assertFalse(lp.hasIpv6UnreachableDefaultRoute());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testHasIpv6UnreachableDefaultRoute() {
         final LinkProperties lp = makeTestObject();
         assertFalse(lp.hasIpv6UnreachableDefaultRoute());
diff --git a/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt b/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt
index 4a4859d..70adbd7 100644
--- a/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt
+++ b/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt
@@ -52,7 +52,6 @@
     }
 
     @Test
-    @IgnoreUpTo(Build.VERSION_CODES.Q)
     @IgnoreAfter(Build.VERSION_CODES.R)
     // Only run this test on Android R.
     // The method - satisfiedBy() has changed to canBeSatisfiedBy() starting from Android R, so the
diff --git a/tests/common/java/android/net/NattKeepalivePacketDataTest.kt b/tests/common/java/android/net/NattKeepalivePacketDataTest.kt
index e5806a6..1148eff 100644
--- a/tests/common/java/android/net/NattKeepalivePacketDataTest.kt
+++ b/tests/common/java/android/net/NattKeepalivePacketDataTest.kt
@@ -82,7 +82,7 @@
         dstPort: Int = NATT_PORT
     ) = NattKeepalivePacketData.nattKeepalivePacket(srcAddress, srcPort, dstAddress, dstPort)
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     fun testConstructor() {
         assertFailsWith<InvalidPacketException>(
             "Dst port is not NATT port should cause exception") {
@@ -132,12 +132,12 @@
         assertEquals(TEST_ADDRV6, packet2.dstAddress)
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     fun testParcel() {
         assertParcelingIsLossless(nattKeepalivePacket())
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     fun testEquals() {
         assertEqualBothWays(nattKeepalivePacket(), nattKeepalivePacket())
         assertNotEquals(nattKeepalivePacket(dstAddress = TEST_SRC_ADDRV4), nattKeepalivePacket())
@@ -146,7 +146,7 @@
         assertNotEquals(nattKeepalivePacket(srcPort = TEST_PORT2), nattKeepalivePacket())
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     fun testHashCode() {
         assertEquals(nattKeepalivePacket().hashCode(), nattKeepalivePacket().hashCode())
     }
diff --git a/tests/common/java/android/net/NetworkAgentConfigTest.kt b/tests/common/java/android/net/NetworkAgentConfigTest.kt
index c05cdbd..d640a73 100644
--- a/tests/common/java/android/net/NetworkAgentConfigTest.kt
+++ b/tests/common/java/android/net/NetworkAgentConfigTest.kt
@@ -16,19 +16,15 @@
 
 package android.net
 
-import android.os.Build
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
 import com.android.modules.utils.build.SdkLevel.isAtLeastS
 import com.android.modules.utils.build.SdkLevel.isAtLeastT
 import com.android.testutils.ConnectivityModuleTest
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.assertParcelingIsLossless
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -36,10 +32,7 @@
 @SmallTest
 @ConnectivityModuleTest
 class NetworkAgentConfigTest {
-    @Rule @JvmField
-    val ignoreRule = DevSdkIgnoreRule()
-
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     fun testParcelNetworkAgentConfig() {
         val config = NetworkAgentConfig.Builder().apply {
             setExplicitlySelected(true)
@@ -58,7 +51,7 @@
         assertParcelingIsLossless(config)
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     fun testBuilder() {
         val testExtraInfo = "mylegacyExtraInfo"
         val config = NetworkAgentConfig.Builder().apply {
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index bec9a4a..3a3459b 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -61,7 +61,6 @@
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
 import static android.os.Process.INVALID_UID;
 
-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.modules.utils.build.SdkLevel.isAtLeastV;
@@ -382,10 +381,9 @@
             netCap.setSubscriptionIds(Set.of(TEST_SUBID1, TEST_SUBID2));
             netCap.setUids(uids);
         }
-        if (isAtLeastR()) {
-            netCap.setOwnerUid(123);
-            netCap.setAdministratorUids(new int[] {5, 11});
-        }
+
+        netCap.setOwnerUid(123);
+        netCap.setAdministratorUids(new int[] {5, 11});
         assertParcelingIsLossless(netCap);
         netCap.setSSID(TEST_SSID);
         testParcelSane(netCap);
@@ -397,10 +395,8 @@
                 .addCapability(NET_CAPABILITY_INTERNET)
                 .addCapability(NET_CAPABILITY_EIMS)
                 .addCapability(NET_CAPABILITY_NOT_METERED);
-        if (isAtLeastR()) {
-            netCap.setRequestorPackageName("com.android.test");
-            netCap.setRequestorUid(9304);
-        }
+        netCap.setRequestorPackageName("com.android.test");
+        netCap.setRequestorUid(9304);
         assertParcelingIsLossless(netCap);
         netCap.setSSID(TEST_SSID);
         testParcelSane(netCap);
@@ -820,16 +816,12 @@
             assertTrue(nc2.hasForbiddenCapability(NET_CAPABILITY_NOT_ROAMING));
         }
 
-        if (isAtLeastR()) {
-            assertTrue(TEST_SSID.equals(nc2.getSsid()));
-        }
-
+        assertTrue(TEST_SSID.equals(nc2.getSsid()));
         nc1.setSSID(DIFFERENT_TEST_SSID);
         nc2.set(nc1);
         assertEquals(nc1, nc2);
-        if (isAtLeastR()) {
-            assertTrue(DIFFERENT_TEST_SSID.equals(nc2.getSsid()));
-        }
+        assertTrue(DIFFERENT_TEST_SSID.equals(nc2.getSsid()));
+
         if (isAtLeastS()) {
             nc1.setUids(uidRanges(10, 13));
         } else {
diff --git a/tests/common/java/android/net/NetworkProviderTest.kt b/tests/common/java/android/net/NetworkProviderTest.kt
index c6a7346..0d35960 100644
--- a/tests/common/java/android/net/NetworkProviderTest.kt
+++ b/tests/common/java/android/net/NetworkProviderTest.kt
@@ -39,6 +39,12 @@
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.TestableNetworkOfferCallback
+import java.util.UUID
+import java.util.concurrent.Executor
+import java.util.concurrent.RejectedExecutionException
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.fail
 import org.junit.After
 import org.junit.Before
 import org.junit.Rule
@@ -47,12 +53,6 @@
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.verifyNoMoreInteractions
-import java.util.UUID
-import java.util.concurrent.Executor
-import java.util.concurrent.RejectedExecutionException
-import kotlin.test.assertEquals
-import kotlin.test.assertNotEquals
-import kotlin.test.fail
 
 private const val DEFAULT_TIMEOUT_MS = 5000L
 private const val DEFAULT_NO_CALLBACK_TIMEOUT_MS = 200L
@@ -62,7 +62,6 @@
 private val PROVIDER_NAME = "NetworkProviderTest"
 
 @RunWith(DevSdkIgnoreRunner::class)
-@IgnoreUpTo(Build.VERSION_CODES.Q)
 @ConnectivityModuleTest
 class NetworkProviderTest {
     @Rule @JvmField
diff --git a/tests/common/java/android/net/NetworkSpecifierTest.kt b/tests/common/java/android/net/NetworkSpecifierTest.kt
index b960417..7edb474 100644
--- a/tests/common/java/android/net/NetworkSpecifierTest.kt
+++ b/tests/common/java/android/net/NetworkSpecifierTest.kt
@@ -15,21 +15,18 @@
  */
 package android.net
 
-import android.os.Build
 import androidx.test.filters.SmallTest
 import com.android.testutils.ConnectivityModuleTest
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
-import org.junit.Test
-import org.junit.runner.RunWith
 import kotlin.test.assertEquals
 import kotlin.test.assertFalse
 import kotlin.test.assertNotEquals
 import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
 
 @SmallTest
 @RunWith(DevSdkIgnoreRunner::class)
-@IgnoreUpTo(Build.VERSION_CODES.Q)
 @ConnectivityModuleTest
 class NetworkSpecifierTest {
     private class TestNetworkSpecifier(
diff --git a/tests/common/java/android/net/NetworkStackTest.java b/tests/common/java/android/net/NetworkStackTest.java
index f8f9c72..13550f9 100644
--- a/tests/common/java/android/net/NetworkStackTest.java
+++ b/tests/common/java/android/net/NetworkStackTest.java
@@ -17,16 +17,11 @@
 
 import static org.junit.Assert.assertEquals;
 
-import android.os.Build;
 import android.os.IBinder;
 
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
-
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -34,16 +29,13 @@
 
 @RunWith(AndroidJUnit4.class)
 public class NetworkStackTest {
-    @Rule
-    public DevSdkIgnoreRule mDevSdkIgnoreRule = new DevSdkIgnoreRule();
-
     @Mock private IBinder mConnectorBinder;
 
     @Before public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testGetService() {
         NetworkStack.setServiceForTest(mConnectorBinder);
         assertEquals(NetworkStack.getService(), mConnectorBinder);
diff --git a/tests/common/java/android/net/NetworkTest.java b/tests/common/java/android/net/NetworkTest.java
index c102cb3..86d2463 100644
--- a/tests/common/java/android/net/NetworkTest.java
+++ b/tests/common/java/android/net/NetworkTest.java
@@ -161,8 +161,7 @@
         assertEquals(16290598925L, three.getNetworkHandle());
     }
 
-    // getNetId() did not exist in Q
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testGetNetId() {
         assertEquals(1234, new Network(1234).getNetId());
         assertEquals(2345, new Network(2345, true).getNetId());
diff --git a/tests/common/java/android/net/RouteInfoTest.java b/tests/common/java/android/net/RouteInfoTest.java
index 5b28b84..154dc4c 100644
--- a/tests/common/java/android/net/RouteInfoTest.java
+++ b/tests/common/java/android/net/RouteInfoTest.java
@@ -31,17 +31,11 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
-import android.os.Build;
-
-import androidx.core.os.BuildCompat;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.testutils.ConnectivityModuleTest;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -53,9 +47,6 @@
 @SmallTest
 @ConnectivityModuleTest
 public class RouteInfoTest {
-    @Rule
-    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
-
     private static final int INVALID_ROUTE_TYPE = -1;
 
     private InetAddress Address(String addr) {
@@ -66,11 +57,6 @@
         return new IpPrefix(prefix);
     }
 
-    private static boolean isAtLeastR() {
-        // BuildCompat.isAtLeastR is documented to return false on release SDKs (including R)
-        return Build.VERSION.SDK_INT > Build.VERSION_CODES.Q || BuildCompat.isAtLeastR();
-    }
-
     @Test
     public void testConstructor() {
         RouteInfo r;
@@ -204,130 +190,108 @@
         assertTrue(r.isDefaultRoute());
         assertTrue(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
+
 
         r = new RouteInfo(Prefix("::/0"), Address("::"), "wlan0");
         assertFalse(r.isHostRoute());
         assertTrue(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertTrue(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(Prefix("192.0.2.0/24"), null, "wlan0");
         assertFalse(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(Prefix("2001:db8::/48"), null, "wlan0");
         assertFalse(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(Prefix("192.0.2.0/32"), Address("0.0.0.0"), "wlan0");
         assertTrue(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(Prefix("2001:db8::/128"), Address("::"), "wlan0");
         assertTrue(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(Prefix("192.0.2.0/32"), null, "wlan0");
         assertTrue(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(Prefix("2001:db8::/128"), null, "wlan0");
         assertTrue(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(Prefix("::/128"), Address("fe80::"), "wlan0");
         assertTrue(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(Prefix("0.0.0.0/32"), Address("192.0.2.1"), "wlan0");
         assertTrue(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(Prefix("0.0.0.0/32"), Address("192.0.2.1"), "wlan0");
         assertTrue(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), RTN_UNREACHABLE);
         assertFalse(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertTrue(r.isIPv4UnreachableDefault());
-            assertFalse(r.isIPv6UnreachableDefault());
-        }
+        assertTrue(r.isIPv4UnreachableDefault());
+        assertFalse(r.isIPv6UnreachableDefault());
 
         r = new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE);
         assertFalse(r.isHostRoute());
         assertFalse(r.isDefaultRoute());
         assertFalse(r.isIPv4Default());
         assertFalse(r.isIPv6Default());
-        if (isAtLeastR()) {
-            assertFalse(r.isIPv4UnreachableDefault());
-            assertTrue(r.isIPv6UnreachableDefault());
-        }
+        assertFalse(r.isIPv4UnreachableDefault());
+        assertTrue(r.isIPv6UnreachableDefault());
     }
 
     @Test
@@ -376,14 +340,14 @@
         assertParcelingIsLossless(r);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testMtuParceling() {
         final RouteInfo r = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::"), "testiface",
                 RTN_UNREACHABLE, 1450 /* mtu */);
         assertParcelingIsLossless(r);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testMtu() {
         RouteInfo r;
         r = new RouteInfo(Prefix("0.0.0.0/0"), Address("0.0.0.0"), "wlan0",
@@ -394,7 +358,7 @@
         assertEquals(0, r.getMtu());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testRouteKey() {
         RouteInfo.RouteKey k1, k2;
         // Only prefix, null gateway and null interface
diff --git a/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
index c90b1aa..8cef6aa 100644
--- a/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
+++ b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
@@ -28,25 +28,18 @@
 import android.net.NetworkStats.SET_DEFAULT
 import android.net.NetworkStats.SET_FOREGROUND
 import android.net.NetworkStats.TAG_NONE
-import android.os.Build
 import androidx.test.filters.SmallTest
-import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.assertNetworkStatsEquals
 import com.android.testutils.assertParcelingIsLossless
+import kotlin.test.assertEquals
 import org.junit.Before
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
-import kotlin.test.assertEquals
 
 @RunWith(JUnit4::class)
 @SmallTest
 class NetworkStatsApiTest {
-    @Rule
-    @JvmField
-    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q)
-
     private val testStatsEmpty = NetworkStats(0L, 0)
 
     // Note that these variables need to be initialized outside of constructor, initialize
diff --git a/tests/common/java/android/net/util/SocketUtilsTest.kt b/tests/common/java/android/net/util/SocketUtilsTest.kt
index aaf97f3..520cf07 100644
--- a/tests/common/java/android/net/util/SocketUtilsTest.kt
+++ b/tests/common/java/android/net/util/SocketUtilsTest.kt
@@ -16,7 +16,6 @@
 
 package android.net.util
 
-import android.os.Build
 import android.system.NetlinkSocketAddress
 import android.system.Os
 import android.system.OsConstants.AF_INET
@@ -27,13 +26,10 @@
 import android.system.PacketSocketAddress
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -44,9 +40,6 @@
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class SocketUtilsTest {
-    @Rule @JvmField
-    val ignoreRule = DevSdkIgnoreRule()
-
     @Test
     fun testMakeNetlinkSocketAddress() {
         val nlAddress = SocketUtils.makeNetlinkSocketAddress(TEST_PORT, RTMGRP_NEIGH)
@@ -67,7 +60,7 @@
         assertTrue("Not PacketSocketAddress object", pkAddress2 is PacketSocketAddress)
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     fun testMakePacketSocketAddress() {
         val pkAddress = SocketUtils.makePacketSocketAddress(
                 ETH_P_ALL, TEST_INDEX, ByteArray(6) { FF_BYTE })
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index 5893de7..bb32052 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -37,6 +37,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.app.Instrumentation;
 import android.app.NotificationManager;
@@ -438,6 +439,42 @@
     }
 
     /**
+     * Asserts whether the network is blocked by accessing bpf maps if command-line tool supports.
+     */
+    void assertNetworkAccessBlockedByBpf(boolean expectBlocked, int uid, boolean metered) {
+        final String result;
+        try {
+            result = executeShellCommand(
+                    "cmd network_stack is-uid-networking-blocked " + uid + " " + metered);
+        } catch (AssertionError e) {
+            // If NetworkStack is too old to support this command, ignore and continue
+            // this test to verify other parts.
+            if (e.getMessage().contains("No shell command implementation.")) {
+                return;
+            }
+            throw e;
+        }
+
+        // Tethering module is too old.
+        if (result.contains("API is unsupported")) {
+            return;
+        }
+
+        assertEquals(expectBlocked, parseBooleanOrThrow(result.trim()));
+    }
+
+    /**
+     * Similar to {@link Boolean#parseBoolean} but throws when the input
+     * is unexpected instead of returning false.
+     */
+    private static boolean parseBooleanOrThrow(@NonNull String s) {
+        // Don't use Boolean.parseBoolean
+        if ("true".equalsIgnoreCase(s)) return true;
+        if ("false".equalsIgnoreCase(s)) return false;
+        throw new IllegalArgumentException("Unexpected: " + s);
+    }
+
+    /**
      * Checks whether the network is available as expected.
      *
      * @return error message with the mismatch (or empty if assertion passed).
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
index 0715e32..ab3cf14 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
@@ -236,16 +236,19 @@
             // Enable restrict background
             setRestrictBackground(true);
             assertBackgroundNetworkAccess(false);
+            assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
             mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
 
             // Add to whitelist
             addRestrictBackgroundWhitelist(mUid);
             assertBackgroundNetworkAccess(true);
+            assertNetworkAccessBlockedByBpf(false, mUid, true /* metered */);
             mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
 
             // Remove from whitelist
             removeRestrictBackgroundWhitelist(mUid);
             assertBackgroundNetworkAccess(false);
+            assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
             mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
         } finally {
             mMeterednessConfiguration.resetNetworkMeteredness();
@@ -257,11 +260,13 @@
                 true /* hasCapability */, NET_CAPABILITY_NOT_METERED);
         try {
             assertBackgroundNetworkAccess(true);
+            assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
             mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
 
             // Disable restrict background, should not trigger callback
             setRestrictBackground(false);
             assertBackgroundNetworkAccess(true);
+            assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
         } finally {
             mMeterednessConfiguration.resetNetworkMeteredness();
         }
@@ -275,11 +280,13 @@
             setBatterySaverMode(true);
             assertBackgroundNetworkAccess(false);
             mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
+            assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
 
             // Disable Power Saver
             setBatterySaverMode(false);
             assertBackgroundNetworkAccess(true);
             mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
+            assertNetworkAccessBlockedByBpf(false, mUid, true /* metered */);
         } finally {
             mMeterednessConfiguration.resetNetworkMeteredness();
         }
@@ -293,11 +300,13 @@
             setBatterySaverMode(true);
             assertBackgroundNetworkAccess(false);
             mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
+            assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
 
             // Disable Power Saver
             setBatterySaverMode(false);
             assertBackgroundNetworkAccess(true);
             mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
+            assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
         } finally {
             mMeterednessConfiguration.resetNetworkMeteredness();
         }
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index b77162c..ea905d5 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -71,6 +71,7 @@
 import android.content.Context;
 import android.net.BpfNetMapsUtils;
 import android.net.INetd;
+import android.net.InetAddresses;
 import android.net.UidOwnerValue;
 import android.os.Build;
 import android.os.ServiceSpecificException;
@@ -87,6 +88,8 @@
 import com.android.net.module.util.Struct.U8;
 import com.android.net.module.util.bpf.CookieTagMapKey;
 import com.android.net.module.util.bpf.CookieTagMapValue;
+import com.android.net.module.util.bpf.IngressDiscardKey;
+import com.android.net.module.util.bpf.IngressDiscardValue;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -102,6 +105,8 @@
 
 import java.io.FileDescriptor;
 import java.io.StringWriter;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -120,6 +125,10 @@
     private static final int TEST_IF_INDEX = 7;
     private static final int NO_IIF = 0;
     private static final int NULL_IIF = 0;
+    private static final Inet4Address TEST_V4_ADDRESS =
+            (Inet4Address) InetAddresses.parseNumericAddress("192.0.2.1");
+    private static final Inet6Address TEST_V6_ADDRESS =
+            (Inet6Address) InetAddresses.parseNumericAddress("2001:db8::1");
     private static final String CHAINNAME = "fw_dozable";
 
     private static final long STATS_SELECT_MAP_A = 0;
@@ -143,11 +152,14 @@
     private final IBpfMap<CookieTagMapKey, CookieTagMapValue> mCookieTagMap =
             spy(new TestBpfMap<>(CookieTagMapKey.class, CookieTagMapValue.class));
     private final IBpfMap<S32, U8> mDataSaverEnabledMap = new TestBpfMap<>(S32.class, U8.class);
+    private final IBpfMap<IngressDiscardKey, IngressDiscardValue> mIngressDiscardMap =
+            new TestBpfMap<>(IngressDiscardKey.class, IngressDiscardValue.class);
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         doReturn(TEST_IF_INDEX).when(mDeps).getIfIndex(TEST_IF_NAME);
+        doReturn(TEST_IF_NAME).when(mDeps).getIfName(TEST_IF_INDEX);
         doReturn(0).when(mDeps).synchronizeKernelRCU();
         BpfNetMaps.setConfigurationMapForTest(mConfigurationMap);
         mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(0));
@@ -158,6 +170,7 @@
         BpfNetMaps.setCookieTagMapForTest(mCookieTagMap);
         BpfNetMaps.setDataSaverEnabledMapForTest(mDataSaverEnabledMap);
         mDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(DATA_SAVER_DISABLED));
+        BpfNetMaps.setIngressDiscardMapForTest(mIngressDiscardMap);
         mBpfNetMaps = new BpfNetMaps(mContext, mNetd, mDeps);
     }
 
@@ -1205,7 +1218,7 @@
     @Test
     @IgnoreAfter(Build.VERSION_CODES.S_V2)
     public void testSetDataSaverEnabledBeforeT() {
-        for (boolean enable : new boolean[] {true, false}) {
+        for (boolean enable : new boolean[]{true, false}) {
             assertThrows(UnsupportedOperationException.class,
                     () -> mBpfNetMaps.setDataSaverEnabled(enable));
         }
@@ -1214,10 +1227,60 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     public void testSetDataSaverEnabled() throws Exception {
-        for (boolean enable : new boolean[] {true, false}) {
+        for (boolean enable : new boolean[]{true, false}) {
             mBpfNetMaps.setDataSaverEnabled(enable);
             assertEquals(enable ? DATA_SAVER_ENABLED : DATA_SAVER_DISABLED,
-                         mDataSaverEnabledMap.getValue(DATA_SAVER_ENABLED_KEY).val);
+                    mDataSaverEnabledMap.getValue(DATA_SAVER_ENABLED_KEY).val);
         }
     }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testSetIngressDiscardRule_V4address() throws Exception {
+        mBpfNetMaps.setIngressDiscardRule(TEST_V4_ADDRESS, TEST_IF_NAME);
+        final IngressDiscardValue val = mIngressDiscardMap.getValue(new IngressDiscardKey(
+                TEST_V4_ADDRESS));
+        assertEquals(TEST_IF_INDEX, val.iif1);
+        assertEquals(TEST_IF_INDEX, val.iif2);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testSetIngressDiscardRule_V6address() throws Exception {
+        mBpfNetMaps.setIngressDiscardRule(TEST_V6_ADDRESS, TEST_IF_NAME);
+        final IngressDiscardValue val =
+                mIngressDiscardMap.getValue(new IngressDiscardKey(TEST_V6_ADDRESS));
+        assertEquals(TEST_IF_INDEX, val.iif1);
+        assertEquals(TEST_IF_INDEX, val.iif2);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testRemoveIngressDiscardRule() throws Exception {
+        mBpfNetMaps.setIngressDiscardRule(TEST_V4_ADDRESS, TEST_IF_NAME);
+        mBpfNetMaps.setIngressDiscardRule(TEST_V6_ADDRESS, TEST_IF_NAME);
+        final IngressDiscardKey v4Key = new IngressDiscardKey(TEST_V4_ADDRESS);
+        final IngressDiscardKey v6Key = new IngressDiscardKey(TEST_V6_ADDRESS);
+        assertTrue(mIngressDiscardMap.containsKey(v4Key));
+        assertTrue(mIngressDiscardMap.containsKey(v6Key));
+
+        mBpfNetMaps.removeIngressDiscardRule(TEST_V4_ADDRESS);
+        assertFalse(mIngressDiscardMap.containsKey(v4Key));
+        assertTrue(mIngressDiscardMap.containsKey(v6Key));
+
+        mBpfNetMaps.removeIngressDiscardRule(TEST_V6_ADDRESS);
+        assertFalse(mIngressDiscardMap.containsKey(v4Key));
+        assertFalse(mIngressDiscardMap.containsKey(v6Key));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testDumpIngressDiscardRule() throws Exception {
+        mBpfNetMaps.setIngressDiscardRule(TEST_V4_ADDRESS, TEST_IF_NAME);
+        mBpfNetMaps.setIngressDiscardRule(TEST_V6_ADDRESS, TEST_IF_NAME);
+        final String dump = getDump();
+        assertDumpContains(dump, TEST_V4_ADDRESS.getHostAddress());
+        assertDumpContains(dump, TEST_V6_ADDRESS.getHostAddress());
+        assertDumpContains(dump, TEST_IF_INDEX + "(" + TEST_IF_NAME + ")");
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
index d9f7f9f..235f7de 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
@@ -21,27 +21,39 @@
 import android.net.LinkProperties
 import android.net.LocalNetworkConfig
 import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_DUN
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
 import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
 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.TRANSPORT_CELLULAR
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
 import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
+import android.net.NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK
 import android.net.RouteInfo
 import android.os.Build
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
-import com.android.testutils.RecorderCallback.CallbackEntry.Available
-import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
-import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
-import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.LocalInfoChanged
 import com.android.testutils.RecorderCallback.CallbackEntry.Lost
 import com.android.testutils.TestableNetworkCallback
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
 import kotlin.test.assertFailsWith
 
+private const val TIMEOUT_MS = 200L
+private const val MEDIUM_TIMEOUT_MS = 1_000L
+private const val LONG_TIMEOUT_MS = 5_000
+
 private fun nc(transport: Int, vararg caps: Int) = NetworkCapabilities.Builder().apply {
     addTransportType(transport)
     caps.forEach {
@@ -60,6 +72,12 @@
     addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
 }
 
+// This allows keeping all the networks connected without having to file individual requests
+// for them.
+private fun keepScore() = FromS(
+        NetworkScore.Builder().setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST).build()
+)
+
 @RunWith(DevSdkIgnoreRunner::class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
 class CSLocalAgentTests : CSTest() {
@@ -93,10 +111,7 @@
                 .build(),
                 lnc = LocalNetworkConfig.Builder().build())
         agent.connect()
-        cb.expect<Available>(agent.network)
-        cb.expect<CapabilitiesChanged>(agent.network)
-        cb.expect<LinkPropertiesChanged>(agent.network)
-        cb.expect<BlockedStatus>(agent.network)
+        cb.expectAvailableCallbacks(agent.network, validated = false)
         agent.sendNetworkCapabilities(NetworkCapabilities.Builder().build())
         cb.expect<Lost>(agent.network)
 
@@ -104,10 +119,7 @@
                 .build(),
                 lnc = null)
         agent2.connect()
-        cb.expect<Available>(agent2.network)
-        cb.expect<CapabilitiesChanged>(agent2.network)
-        cb.expect<LinkPropertiesChanged>(agent2.network)
-        cb.expect<BlockedStatus>(agent2.network)
+        cb.expectAvailableCallbacks(agent2.network, validated = false)
         agent2.sendNetworkCapabilities(NetworkCapabilities.Builder()
                 .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
                 .build())
@@ -125,16 +137,18 @@
                 cb)
 
         // Set up a local agent that should forward its traffic to the best DUN upstream.
-        val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
+        val localAgent = Agent(
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
                 lp = lp("local0"),
                 lnc = LocalNetworkConfig.Builder().build(),
         )
         localAgent.connect()
 
-        cb.expect<Available>(localAgent.network)
-        cb.expect<CapabilitiesChanged>(localAgent.network)
-        cb.expect<LinkPropertiesChanged>(localAgent.network)
-        cb.expect<BlockedStatus>(localAgent.network)
+        cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+        val wifiAgent = Agent(score = keepScore(), lp = lp("wifi0"),
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+        wifiAgent.connect()
 
         val newLnc = LocalNetworkConfig.Builder()
                 .setUpstreamSelector(NetworkRequest.Builder()
@@ -143,6 +157,319 @@
                 .build()
         localAgent.sendLocalNetworkConfig(newLnc)
 
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgent.network
+        }
+
+        localAgent.sendLocalNetworkConfig(LocalNetworkConfig.Builder().build())
+        cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+
+        localAgent.sendLocalNetworkConfig(newLnc)
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgent.network
+        }
+
+        wifiAgent.disconnect()
+        cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+
         localAgent.disconnect()
     }
+
+    @Test
+    fun testUnregisterUpstreamAfterReplacement_SameIfaceName() {
+        doTestUnregisterUpstreamAfterReplacement(true)
+    }
+
+    @Test
+    fun testUnregisterUpstreamAfterReplacement_DifferentIfaceName() {
+        doTestUnregisterUpstreamAfterReplacement(false)
+    }
+
+    fun doTestUnregisterUpstreamAfterReplacement(sameIfaceName: Boolean) {
+        deps.setBuildSdk(VERSION_V)
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
+
+        // Set up a local agent that should forward its traffic to the best wifi upstream.
+        val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
+                lp = lp("local0"),
+                lnc = LocalNetworkConfig.Builder()
+                .setUpstreamSelector(NetworkRequest.Builder()
+                        .addTransportType(TRANSPORT_WIFI)
+                        .build())
+                .build(),
+                score = FromS(NetworkScore.Builder()
+                        .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
+                        .build())
+        )
+        localAgent.connect()
+
+        cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+        val wifiAgent = Agent(lp = lp("wifi0"),
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+        wifiAgent.connect()
+
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgent.network
+        }
+
+        clearInvocations(netd)
+        val inOrder = inOrder(netd)
+        wifiAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+        waitForIdle()
+        inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi0")
+        inOrder.verify(netd).networkDestroy(wifiAgent.network.netId)
+
+        val wifiIface2 = if (sameIfaceName) "wifi0" else "wifi1"
+        val wifiAgent2 = Agent(lp = lp(wifiIface2),
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+        wifiAgent2.connect()
+
+        cb.expectAvailableCallbacks(wifiAgent2.network, validated = false)
+        cb.expect<LocalInfoChanged> { it.info.upstreamNetwork == wifiAgent2.network }
+        cb.expect<Lost> { it.network == wifiAgent.network }
+
+        inOrder.verify(netd).ipfwdAddInterfaceForward("local0", wifiIface2)
+        if (sameIfaceName) {
+            inOrder.verify(netd, never()).ipfwdRemoveInterfaceForward(any(), any())
+        }
+    }
+
+    @Test
+    fun testUnregisterUpstreamAfterReplacement_neverReplaced() {
+        deps.setBuildSdk(VERSION_V)
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
+
+        // Set up a local agent that should forward its traffic to the best wifi upstream.
+        val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
+                lp = lp("local0"),
+                lnc = LocalNetworkConfig.Builder()
+                        .setUpstreamSelector(NetworkRequest.Builder()
+                                .addTransportType(TRANSPORT_WIFI)
+                                .build())
+                        .build(),
+                score = FromS(NetworkScore.Builder()
+                        .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
+                        .build())
+        )
+        localAgent.connect()
+
+        cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+        val wifiAgent = Agent(lp = lp("wifi0"),
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+        wifiAgent.connect()
+
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgent.network
+        }
+
+        clearInvocations(netd)
+        wifiAgent.unregisterAfterReplacement(TIMEOUT_MS.toInt())
+        waitForIdle()
+        verify(netd).networkDestroy(wifiAgent.network.netId)
+        verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi0")
+
+        cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+        cb.expect<Lost> { it.network == wifiAgent.network }
+    }
+
+    @Test
+    fun testUnregisterLocalAgentAfterReplacement() {
+        deps.setBuildSdk(VERSION_V)
+
+        val localCb = TestableNetworkCallback()
+        cm.requestNetwork(NetworkRequest.Builder().clearCapabilities()
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .build(),
+                localCb)
+
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
+
+        val localNc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK)
+        val lnc = LocalNetworkConfig.Builder()
+                .setUpstreamSelector(NetworkRequest.Builder()
+                        .addTransportType(TRANSPORT_WIFI)
+                        .build())
+                .build()
+        val localScore = FromS(NetworkScore.Builder().build())
+
+        // Set up a local agent that should forward its traffic to the best wifi upstream.
+        val localAgent = Agent(nc = localNc, lp = lp("local0"), lnc = lnc, score = localScore)
+        localAgent.connect()
+
+        localCb.expectAvailableCallbacks(localAgent.network, validated = false)
+        cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+        val wifiAgent = Agent(lp = lp("wifi0"), nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+        wifiAgent.connect()
+
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+        listOf(cb, localCb).forEach {
+            it.expect<LocalInfoChanged>(localAgent.network) {
+                it.info.upstreamNetwork == wifiAgent.network
+            }
+        }
+
+        verify(netd).ipfwdAddInterfaceForward("local0", "wifi0")
+
+        localAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+
+        val localAgent2 = Agent(nc = localNc, lp = lp("local0"), lnc = lnc, score = localScore)
+        localAgent2.connect()
+
+        localCb.expectAvailableCallbacks(localAgent2.network,
+                validated = false, upstream = wifiAgent.network)
+        cb.expectAvailableCallbacks(localAgent2.network,
+                validated = false, upstream = wifiAgent.network)
+        cb.expect<Lost> { it.network == localAgent.network }
+    }
+
+    @Test
+    fun testDestroyedNetworkAsSelectedUpstream() {
+        deps.setBuildSdk(VERSION_V)
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
+
+        val wifiAgent = Agent(lp = lp("wifi0"), nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+        // Unregister wifi pending replacement, then set up a local agent that would have
+        // this network as its upstream.
+        wifiAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+        val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
+                lp = lp("local0"),
+                lnc = LocalNetworkConfig.Builder()
+                        .setUpstreamSelector(NetworkRequest.Builder()
+                                .addTransportType(TRANSPORT_WIFI)
+                                .build())
+                        .build(),
+                score = FromS(NetworkScore.Builder()
+                        .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
+                        .build())
+        )
+
+        // Connect the local agent. The zombie wifi is its upstream, but the stack doesn't
+        // tell netd to add the forward since the wifi0 interface has gone.
+        localAgent.connect()
+        cb.expectAvailableCallbacks(localAgent.network,
+                validated = false, upstream = wifiAgent.network)
+
+        verify(netd, never()).ipfwdAddInterfaceForward("local0", "wifi0")
+
+        // Disconnect wifi without a replacement. Expect an update with upstream null.
+        wifiAgent.disconnect()
+        verify(netd, never()).ipfwdAddInterfaceForward("local0", "wifi0")
+        cb.expect<LocalInfoChanged> { it.info.upstreamNetwork == null }
+    }
+
+    @Test
+    fun testForwardingRules() {
+        deps.setBuildSdk(VERSION_V)
+        // Set up a local agent that should forward its traffic to the best DUN upstream.
+        val lnc = LocalNetworkConfig.Builder()
+                .setUpstreamSelector(NetworkRequest.Builder()
+                        .addCapability(NET_CAPABILITY_DUN)
+                        .build())
+                .build()
+        val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
+                lp = lp("local0"),
+                lnc = lnc,
+                score = FromS(NetworkScore.Builder()
+                        .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
+                        .build())
+        )
+        localAgent.connect()
+
+        val wifiAgent = Agent(score = keepScore(), lp = lp("wifi0"),
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+        val cellAgentDun = Agent(score = keepScore(), lp = lp("cell0"),
+                nc = nc(TRANSPORT_CELLULAR, NET_CAPABILITY_INTERNET, NET_CAPABILITY_DUN))
+        val wifiAgentDun = Agent(score = keepScore(), lp = lp("wifi1"),
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET, NET_CAPABILITY_DUN))
+
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .build(),
+                cb)
+        cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+        val inOrder = inOrder(netd)
+        inOrder.verify(netd, never()).ipfwdAddInterfaceForward(any(), any())
+        cb.assertNoCallback()
+
+        wifiAgent.connect()
+        inOrder.verify(netd, never()).ipfwdAddInterfaceForward(any(), any())
+        cb.assertNoCallback()
+
+        cellAgentDun.connect()
+        inOrder.verify(netd).ipfwdEnableForwarding(any())
+        inOrder.verify(netd).ipfwdAddInterfaceForward("local0", "cell0")
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == cellAgentDun.network
+        }
+
+        wifiAgentDun.connect()
+        inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "cell0")
+        inOrder.verify(netd).ipfwdAddInterfaceForward("local0", "wifi1")
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgentDun.network
+        }
+
+        // Make sure sending the same config again doesn't do anything
+        repeat(5) {
+            localAgent.sendLocalNetworkConfig(lnc)
+        }
+        inOrder.verifyNoMoreInteractions()
+
+        wifiAgentDun.disconnect()
+        cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == cellAgentDun.network
+        }
+        inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi1")
+        // This can take a little bit of time because it needs to wait for the rematch
+        inOrder.verify(netd, timeout(MEDIUM_TIMEOUT_MS)).ipfwdAddInterfaceForward("local0", "cell0")
+
+        cellAgentDun.disconnect()
+        inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "cell0")
+        inOrder.verify(netd).ipfwdDisableForwarding(any())
+        cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+
+        val wifiAgentDun2 = Agent(score = keepScore(), lp = lp("wifi2"),
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET, NET_CAPABILITY_DUN))
+        wifiAgentDun2.connect()
+        inOrder.verify(netd).ipfwdEnableForwarding(any())
+        inOrder.verify(netd).ipfwdAddInterfaceForward("local0", "wifi2")
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgentDun2.network
+        }
+
+        wifiAgentDun2.disconnect()
+        inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi2")
+        inOrder.verify(netd).ipfwdDisableForwarding(any())
+        cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+
+        val wifiAgentDun3 = Agent(score = keepScore(), lp = lp("wifi3"),
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET, NET_CAPABILITY_DUN))
+        wifiAgentDun3.connect()
+        inOrder.verify(netd).ipfwdEnableForwarding(any())
+        inOrder.verify(netd).ipfwdAddInterfaceForward("local0", "wifi3")
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgentDun3.network
+        }
+
+        localAgent.disconnect()
+        inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi3")
+        inOrder.verify(netd).ipfwdDisableForwarding(any())
+        cb.expect<Lost>(localAgent.network)
+        cb.assertNoCallback()
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
index f903e51..013a749 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -168,6 +168,7 @@
     }
 
     fun unregisterAfterReplacement(timeoutMs: Int) = agent.unregisterAfterReplacement(timeoutMs)
+
     fun sendLocalNetworkConfig(lnc: LocalNetworkConfig) = agent.sendLocalNetworkConfig(lnc)
     fun sendNetworkCapabilities(nc: NetworkCapabilities) = agent.sendNetworkCapabilities(nc)
 }
diff --git a/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
index bda9373..520acbd 100644
--- a/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
+++ b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
@@ -43,9 +43,10 @@
     /** @hide */
     public static final int LENGTH_TIMESTAMP = Long.BYTES;
 
-    private static final long TICKS_UPPER_BOUND = 0x8000;
+    private static final int TICKS_UPPER_BOUND = 0x8000;
 
-    private final Instant mInstant;
+    private final long mSeconds;
+    private final int mTicks;
     private final boolean mIsAuthoritativeSource;
 
     /**
@@ -55,18 +56,34 @@
      * {@link instant#getNano()} based on frequency of 32768 Hz, and {@code isAuthoritativeSource}
      * is set to {@code true}.
      *
+     * <p>Note that this conversion can lose precision and a value returned by {@link #toInstant}
+     * may not equal exactly the {@code instant}.
+     *
      * @throws IllegalArgumentException if {@code instant.getEpochSecond()} is larger than {@code
      *     0xffffffffffffL}
+     * @see toInstant
      */
     @NonNull
     public static OperationalDatasetTimestamp fromInstant(@NonNull Instant instant) {
-        return new OperationalDatasetTimestamp(instant, /* isAuthoritativeSource= */ true);
+        int ticks = getRoundedTicks(instant.getNano());
+        long seconds = instant.getEpochSecond() + ticks / TICKS_UPPER_BOUND;
+        // the rounded ticks can be 0x8000 if instant.getNano() >= 999984742
+        ticks = ticks % TICKS_UPPER_BOUND;
+        return new OperationalDatasetTimestamp(seconds, ticks, true /* isAuthoritativeSource */);
     }
 
-    /** Converts this {@link OperationalDatasetTimestamp} object to an {@link Instant}. */
+    /**
+     * Converts this {@link OperationalDatasetTimestamp} object to an {@link Instant}.
+     *
+     * <p>Note that the return value may not equal exactly the {@code instant} if this object is
+     * created with {@link #fromInstant}.
+     *
+     * @see fromInstant
+     */
     @NonNull
     public Instant toInstant() {
-        return mInstant;
+        long nanos = Math.round((double) mTicks * 1000000000L / TICKS_UPPER_BOUND);
+        return Instant.ofEpochSecond(mSeconds, nanos);
     }
 
     /**
@@ -100,10 +117,7 @@
     public byte[] toTlvValue() {
         byte[] tlv = new byte[LENGTH_TIMESTAMP];
         ByteBuffer buffer = ByteBuffer.wrap(tlv);
-        long encodedValue =
-                (mInstant.getEpochSecond() << 16)
-                        | ((mInstant.getNano() * TICKS_UPPER_BOUND / 1000000000L) << 1)
-                        | (mIsAuthoritativeSource ? 1 : 0);
+        long encodedValue = (mSeconds << 16) | (mTicks << 1) | (mIsAuthoritativeSource ? 1 : 0);
         buffer.putLong(encodedValue);
         return tlv;
     }
@@ -125,10 +139,6 @@
             @IntRange(from = 0x0, to = 0xffffffffffffL) long seconds,
             @IntRange(from = 0x0, to = 0x7fff) int ticks,
             boolean isAuthoritativeSource) {
-        this(makeInstant(seconds, ticks), isAuthoritativeSource);
-    }
-
-    private static Instant makeInstant(long seconds, int ticks) {
         checkArgument(
                 seconds >= 0 && seconds <= 0xffffffffffffL,
                 "seconds exceeds allowed range (seconds = %d,"
@@ -138,25 +148,8 @@
                 ticks >= 0 && ticks <= 0x7fff,
                 "ticks exceeds allowed ranged (ticks = %d, allowedRange" + " = [0x0, 0x7fff])",
                 ticks);
-        long nanos = Math.round((double) ticks * 1000000000L / TICKS_UPPER_BOUND);
-        return Instant.ofEpochSecond(seconds, nanos);
-    }
-
-    /**
-     * Creates new {@link OperationalDatasetTimestamp} object.
-     *
-     * @throws IllegalArgumentException if {@code instant.getEpochSecond()} is larger than {@code
-     *     0xffffffffffffL}
-     */
-    private OperationalDatasetTimestamp(@NonNull Instant instant, boolean isAuthoritativeSource) {
-        requireNonNull(instant, "instant cannot be null");
-        long seconds = instant.getEpochSecond();
-        checkArgument(
-                seconds >= 0 && seconds <= 0xffffffffffffL,
-                "instant seconds exceeds allowed range (seconds = %d, allowedRange = [0x0,"
-                        + " 0xffffffffffffL])",
-                seconds);
-        mInstant = instant;
+        mSeconds = seconds;
+        mTicks = ticks;
         mIsAuthoritativeSource = isAuthoritativeSource;
     }
 
@@ -171,13 +164,12 @@
 
     /** Returns the seconds portion of the timestamp. */
     public @IntRange(from = 0x0, to = 0xffffffffffffL) long getSeconds() {
-        return mInstant.getEpochSecond() + getRoundedTicks(mInstant.getNano()) / TICKS_UPPER_BOUND;
+        return mSeconds;
     }
 
     /** Returns the ticks portion of the timestamp. */
     public @IntRange(from = 0x0, to = 0x7fff) int getTicks() {
-        // the rounded ticks can be 0x8000 if mInstant.getNano() >= 999984742
-        return (int) (getRoundedTicks(mInstant.getNano()) % TICKS_UPPER_BOUND);
+        return mTicks;
     }
 
     /** Returns {@code true} if the timestamp comes from an authoritative source. */
@@ -208,13 +200,14 @@
             return false;
         } else {
             OperationalDatasetTimestamp otherTimestamp = (OperationalDatasetTimestamp) other;
-            return mInstant.equals(otherTimestamp.mInstant)
+            return mSeconds == otherTimestamp.mSeconds
+                    && mTicks == otherTimestamp.mTicks
                     && mIsAuthoritativeSource == otherTimestamp.mIsAuthoritativeSource;
         }
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mInstant, mIsAuthoritativeSource);
+        return Objects.hash(mSeconds, mTicks, mIsAuthoritativeSource);
     }
 }
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index 7575757..fc77b1a 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -31,7 +31,7 @@
  * Provides the primary API for controlling all aspects of a Thread network.
  *
  * @hide
-*/
+ */
 @FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
 @SystemApi
 public final class ThreadNetworkController {
diff --git a/thread/scripts/make-pretty.sh b/thread/scripts/make-pretty.sh
new file mode 100755
index 0000000..e4bd459
--- /dev/null
+++ b/thread/scripts/make-pretty.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+
+GOOGLE_JAVA_FORMAT=$SCRIPT_DIR/../../../../../prebuilts/tools/common/google-java-format/google-java-format
+
+$GOOGLE_JAVA_FORMAT --aosp -i $(find $SCRIPT_DIR/../ -name "*.java")
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index ce770e0..b75b8e6 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -44,6 +44,7 @@
     libs: [
         "android.test.base",
         "android.test.runner",
+        "framework-connectivity-flagged-apis"
     ],
     // Test coverage system runs on different devices. Need to
     // compile for all architectures.
diff --git a/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java b/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
index 32063fc..2244a89 100644
--- a/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
+++ b/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
@@ -27,6 +27,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.time.Instant;
+
 /** Unit tests for {@link OperationalDatasetTimestamp}. */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -58,4 +60,22 @@
 
         assertThat(timestamp2).isEqualTo(timestamp1);
     }
+
+    @Test
+    public void toTlvValue_timestampFromInstant_conversionIsLossLess() {
+        // This results in ticks = 999938900 / 1000000000 * 32768 = 32765.9978752 ~= 32766.
+        // The ticks 32766 is then converted back to 999938964.84375 ~= 999938965 nanoseconds.
+        // A wrong implementation may save Instant.getNano() and compare against the nanoseconds
+        // and results in precision loss when converted between OperationalDatasetTimestamp and the
+        // TLV values.
+        OperationalDatasetTimestamp timestamp1 =
+                OperationalDatasetTimestamp.fromInstant(Instant.ofEpochSecond(100, 999938900));
+
+        OperationalDatasetTimestamp timestamp2 =
+                OperationalDatasetTimestamp.fromTlvValue(timestamp1.toTlvValue());
+
+        assertThat(timestamp2.getSeconds()).isEqualTo(100);
+        assertThat(timestamp2.getTicks()).isEqualTo(32766);
+        assertThat(timestamp2).isEqualTo(timestamp1);
+    }
 }