Merge "Expose additional Cronet APIs in AOSP."
diff --git a/Cronet/tests/common/Android.bp b/Cronet/tests/common/Android.bp
index 1ed0881..f8bdb08 100644
--- a/Cronet/tests/common/Android.bp
+++ b/Cronet/tests/common/Android.bp
@@ -35,5 +35,5 @@
         "NetHttpTestsLibPreJarJar",
     ],
     jarjar_rules: ":framework-tethering-jarjar-rules",
-    compile_multilib: "both",
+    compile_multilib: "both", // Include both the 32 and 64 bit versions
 }
diff --git a/Cronet/tests/cts/Android.bp b/Cronet/tests/cts/Android.bp
index 9a5ed89..d260694 100644
--- a/Cronet/tests/cts/Android.bp
+++ b/Cronet/tests/cts/Android.bp
@@ -49,12 +49,14 @@
         "src/**/*.kt",
     ],
     static_libs: [
+        "androidx.test.ext.junit",
         "androidx.test.rules",
         "androidx.core_core",
         "ctstestrunner-axt",
         "ctstestserver",
         "junit",
         "hamcrest-library",
+        "kotlin-test",
     ],
     libs: [
         "android.test.runner",
@@ -73,7 +75,6 @@
         "CronetTestJavaDefaults",
     ],
     sdk_version: "test_current",
-    compile_multilib: "both", // Include both the 32 and 64 bit versions
     static_libs: ["CtsNetHttpTestsLib"],
     // Tag this as a cts test artifact
     test_suites: [
diff --git a/Cronet/tests/cts/src/android/net/http/cts/CallbackExceptionTest.kt b/Cronet/tests/cts/src/android/net/http/cts/CallbackExceptionTest.kt
new file mode 100644
index 0000000..e17b63f
--- /dev/null
+++ b/Cronet/tests/cts/src/android/net/http/cts/CallbackExceptionTest.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.http.cts
+
+import android.content.Context
+import android.net.http.CallbackException
+import android.net.http.HttpEngine
+import android.net.http.cts.util.HttpCtsTestServer
+import android.net.http.cts.util.TestUrlRequestCallback
+import android.net.http.cts.util.TestUrlRequestCallback.FailureType
+import android.net.http.cts.util.TestUrlRequestCallback.ResponseStep
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class CallbackExceptionTest {
+
+    @Test
+    fun testCallbackException_returnsInputParameters() {
+        val message = "failed"
+        val cause = Throwable("exception")
+        val callbackException = object : CallbackException(message, cause) {}
+
+        assertEquals(message, callbackException.message)
+        assertSame(cause, callbackException.cause)
+    }
+
+    @Test
+    fun testCallbackException_thrownFromUrlRequest() {
+        val context: Context = ApplicationProvider.getApplicationContext()
+        val server = HttpCtsTestServer(context)
+        val httpEngine = HttpEngine.Builder(context).build()
+        val callback = TestUrlRequestCallback()
+        callback.setFailure(FailureType.THROW_SYNC, ResponseStep.ON_RESPONSE_STARTED)
+        val request = httpEngine
+            .newUrlRequestBuilder(server.successUrl, callback, callback.executor)
+            .build()
+
+        request.start()
+        callback.blockForDone()
+
+        assertTrue(request.isDone)
+        assertIs<CallbackException>(callback.mError)
+        server.shutdown()
+        httpEngine.shutdown()
+    }
+}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/NetworkExceptionTest.kt b/Cronet/tests/cts/src/android/net/http/cts/NetworkExceptionTest.kt
new file mode 100644
index 0000000..a2611e4
--- /dev/null
+++ b/Cronet/tests/cts/src/android/net/http/cts/NetworkExceptionTest.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.http.cts
+
+import android.net.http.HttpEngine
+import android.net.http.NetworkException
+import android.net.http.cts.util.TestUrlRequestCallback
+import androidx.test.core.app.ApplicationProvider
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+import org.junit.Test
+
+class NetworkExceptionTest {
+
+    @Test
+    fun testNetworkException_returnsInputParameters() {
+        val message = "failed"
+        val cause = Throwable("thrown")
+        val networkException =
+            object : NetworkException(message, cause) {
+                override fun getErrorCode() = 0
+                override fun isImmediatelyRetryable() = false
+            }
+
+        assertEquals(message, networkException.message)
+        assertSame(cause, networkException.cause)
+    }
+
+    @Test
+    fun testNetworkException_thrownFromUrlRequest() {
+        val httpEngine = HttpEngine.Builder(ApplicationProvider.getApplicationContext()).build()
+        val callback = TestUrlRequestCallback()
+        val request =
+            httpEngine.newUrlRequestBuilder("http://localhost", callback, callback.executor).build()
+
+        request.start()
+        callback.blockForDone()
+
+        assertTrue(request.isDone)
+        assertIs<NetworkException>(callback.mError)
+        httpEngine.shutdown()
+    }
+}
diff --git a/Cronet/tests/mts/Android.bp b/Cronet/tests/mts/Android.bp
index cfafc5b..1cabd63 100644
--- a/Cronet/tests/mts/Android.bp
+++ b/Cronet/tests/mts/Android.bp
@@ -37,7 +37,6 @@
         "CronetTestJavaDefaults",
         "mts-target-sdk-version-current",
      ],
-     compile_multilib: "both",
      sdk_version: "test_current",
      static_libs: ["NetHttpTestsLibPreJarJar"],
      jarjar_rules: ":framework-tethering-jarjar-rules",
diff --git a/Tethering/common/TetheringLib/cronet_enabled/api/current.txt b/Tethering/common/TetheringLib/cronet_enabled/api/current.txt
index bff5cf5..0ed1e9f 100644
--- a/Tethering/common/TetheringLib/cronet_enabled/api/current.txt
+++ b/Tethering/common/TetheringLib/cronet_enabled/api/current.txt
@@ -92,6 +92,7 @@
   }
 
   public abstract class HttpEngine {
+    method public void bindToNetwork(@Nullable android.net.Network);
     method public abstract java.net.URLStreamHandlerFactory createURLStreamHandlerFactory();
     method public static String getVersionString();
     method public abstract android.net.http.BidirectionalStream.Builder newBidirectionalStreamBuilder(String, android.net.http.BidirectionalStream.Callback, java.util.concurrent.Executor);
@@ -194,6 +195,7 @@
   public abstract static class UrlRequest.Builder {
     method public abstract android.net.http.UrlRequest.Builder addHeader(String, String);
     method public abstract android.net.http.UrlRequest.Builder allowDirectExecutor();
+    method public abstract android.net.http.UrlRequest.Builder bindToNetwork(@Nullable android.net.Network);
     method public abstract android.net.http.UrlRequest build();
     method public abstract android.net.http.UrlRequest.Builder disableCache();
     method public abstract android.net.http.UrlRequest.Builder setHttpMethod(String);
diff --git a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
index c452e55..775c36f 100644
--- a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
+++ b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
@@ -88,13 +88,13 @@
     private static final int MIN_RTR_ADV_INTERVAL_SEC = 300;
     private static final int MAX_RTR_ADV_INTERVAL_SEC = 600;
     // In general, router, prefix, and DNS lifetimes are all advised to be
-    // greater than or equal to 3 * MAX_RTR_ADV_INTERVAL.  Here, we double
+    // greater than or equal to 3 * MAX_RTR_ADV_INTERVAL.  Here, we quadruple
     // that to allow for multicast packet loss.
     //
     // This MAX_RTR_ADV_INTERVAL_SEC and DEFAULT_LIFETIME are also consistent
     // with the https://tools.ietf.org/html/rfc7772#section-4 discussion of
     // "approximately 7 RAs per hour".
-    private static final int DEFAULT_LIFETIME = 6 * MAX_RTR_ADV_INTERVAL_SEC;
+    private static final int DEFAULT_LIFETIME = 12 * MAX_RTR_ADV_INTERVAL_SEC;
     // From https://tools.ietf.org/html/rfc4861#section-10 .
     private static final int MIN_DELAY_BETWEEN_RAS_SEC = 3;
     // Both initial and final RAs, but also for changes in RA contents.
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
index 11e3dc0..5e08aba 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -21,8 +21,7 @@
     name: "TetheringIntegrationTestsDefaults",
     defaults: ["framework-connectivity-test-defaults"],
     srcs: [
-        "src/**/*.java",
-        "src/**/*.kt",
+        "base/**/*.java",
     ],
     min_sdk_version: "30",
     static_libs: [
@@ -47,6 +46,16 @@
     ],
 }
 
+android_library {
+    name: "TetheringIntegrationTestsBaseLib",
+    target_sdk_version: "current",
+    platform_apis: true,
+    defaults: ["TetheringIntegrationTestsDefaults"],
+    visibility: [
+        "//packages/modules/Connectivity/Tethering/tests/mts",
+    ]
+}
+
 // Library including tethering integration tests targeting the latest stable SDK.
 // Use with NetworkStackJarJarRules.
 android_library {
@@ -54,6 +63,9 @@
     target_sdk_version: "33",
     platform_apis: true,
     defaults: ["TetheringIntegrationTestsDefaults"],
+    srcs: [
+        "src/**/*.java",
+    ],
     visibility: [
         "//packages/modules/Connectivity/tests/cts/tethering",
         "//packages/modules/Connectivity/tests:__subpackages__",
@@ -68,12 +80,16 @@
     target_sdk_version: "current",
     platform_apis: true,
     defaults: ["TetheringIntegrationTestsDefaults"],
+    srcs: [
+        "src/**/*.java",
+    ],
     visibility: [
         "//packages/modules/Connectivity/tests/cts/tethering",
         "//packages/modules/Connectivity/Tethering/tests/mts",
     ]
 }
 
+// TODO: remove because TetheringIntegrationTests has been covered by ConnectivityCoverageTests.
 android_test {
     name: "TetheringIntegrationTests",
     platform_apis: true,
@@ -81,6 +97,9 @@
     test_suites: [
         "device-tests",
     ],
+    srcs: [
+        "src/**/*.java",
+    ],
     compile_multilib: "both",
     jarjar_rules: ":NetworkStackJarJarRules",
 }
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
similarity index 100%
rename from Tethering/tests/integration/src/android/net/EthernetTetheringTestBase.java
rename to Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
diff --git a/Tethering/tests/integration/src/android/net/TetheringTester.java b/Tethering/tests/integration/base/android/net/TetheringTester.java
similarity index 100%
rename from Tethering/tests/integration/src/android/net/TetheringTester.java
rename to Tethering/tests/integration/base/android/net/TetheringTester.java
diff --git a/Tethering/tests/integration/src/android/net/CtsEthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
similarity index 78%
rename from Tethering/tests/integration/src/android/net/CtsEthernetTetheringTest.java
rename to Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index aea6728..fb4b9fa 100644
--- a/Tethering/tests/integration/src/android/net/CtsEthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -16,7 +16,6 @@
 
 package android.net;
 
-import static android.Manifest.permission.DUMP;
 import static android.net.InetAddresses.parseNumericAddress;
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
 import static android.net.TetheringManager.TETHERING_ETHERNET;
@@ -26,7 +25,6 @@
 import static android.system.OsConstants.ICMP_ECHO;
 import static android.system.OsConstants.ICMP_ECHOREPLY;
 import static android.system.OsConstants.IPPROTO_ICMP;
-import static android.system.OsConstants.IPPROTO_UDP;
 
 import static com.android.net.module.util.ConnectivityUtils.isIPv6ULA;
 import static com.android.net.module.util.HexDump.dumpHexString;
@@ -39,39 +37,28 @@
 import static com.android.net.module.util.NetworkStackConstants.IPV4_CHECKSUM_OFFSET;
 import static com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
 import static com.android.net.module.util.NetworkStackConstants.IPV4_LENGTH_OFFSET;
-import static com.android.testutils.DeviceInfoUtils.KVersion;
-import static com.android.testutils.TestPermissionUtil.runAsShell;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
-import android.content.Context;
 import android.net.TetheringManager.TetheringRequest;
 import android.net.TetheringTester.TetheredDevice;
 import android.os.Build;
 import android.os.SystemClock;
 import android.os.SystemProperties;
-import android.os.VintfRuntimeInfo;
 import android.util.Log;
-import android.util.Pair;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.test.filters.MediumTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.net.module.util.BpfDump;
 import com.android.net.module.util.Ipv6Utils;
 import com.android.net.module.util.Struct;
-import com.android.net.module.util.bpf.Tether4Key;
-import com.android.net.module.util.bpf.Tether4Value;
-import com.android.net.module.util.bpf.TetherStatsKey;
-import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.net.module.util.structs.EthernetHeader;
 import com.android.net.module.util.structs.Icmpv4Header;
 import com.android.net.module.util.structs.Ipv4Header;
@@ -79,8 +66,6 @@
 import com.android.net.module.util.structs.UdpHeader;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
-import com.android.testutils.DeviceInfoUtils;
-import com.android.testutils.DumpTestUtils;
 import com.android.testutils.TapPacketReader;
 
 import org.junit.Rule;
@@ -96,9 +81,7 @@
 import java.nio.ByteBuffer;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.Random;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
@@ -106,34 +89,13 @@
 
 @RunWith(AndroidJUnit4.class)
 @MediumTest
-public class CtsEthernetTetheringTest extends EthernetTetheringTestBase {
+public class EthernetTetheringTest extends EthernetTetheringTestBase {
     @Rule
     public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
 
-    private static final String TAG = CtsEthernetTetheringTest.class.getSimpleName();
-
-    private static final int DUMP_POLLING_MAX_RETRY = 100;
-    private static final int DUMP_POLLING_INTERVAL_MS = 50;
-    // Kernel treats a confirmed UDP connection which active after two seconds as stream mode.
-    // See upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5.
-    private static final int UDP_STREAM_TS_MS = 2000;
-    // Give slack time for waiting UDP stream mode because handling conntrack event in user space
-    // may not in precise time. Used to reduce the flaky rate.
-    private static final int UDP_STREAM_SLACK_MS = 500;
-    // Per RX UDP packet size: iphdr (20) + udphdr (8) + payload (2) = 30 bytes.
-    private static final int RX_UDP_PACKET_SIZE = 30;
-    private static final int RX_UDP_PACKET_COUNT = 456;
-    // Per TX UDP packet size: ethhdr (14) + iphdr (20) + udphdr (8) + payload (2) = 44 bytes.
-    private static final int TX_UDP_PACKET_SIZE = 44;
-    private static final int TX_UDP_PACKET_COUNT = 123;
+    private static final String TAG = EthernetTetheringTest.class.getSimpleName();
 
     private static final short DNS_PORT = 53;
-
-    private static final String DUMPSYS_TETHERING_RAWMAP_ARG = "bpfRawMap";
-    private static final String DUMPSYS_RAWMAP_ARG_STATS = "--stats";
-    private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
-    private static final String LINE_DELIMITER = "\\n";
-
     private static final short ICMPECHO_CODE = 0x0;
     private static final short ICMPECHO_ID = 0x0;
     private static final short ICMPECHO_SEQ = 0x0;
@@ -529,7 +491,7 @@
     // remote ip              public ip                           private ip
     // 8.8.8.8:443            <Upstream ip>:9876                  <TetheredDevice ip>:9876
     //
-    private void runUdp4Test(boolean verifyBpf) throws Exception {
+    private void runUdp4Test() throws Exception {
         final TetheringTester tester = initTetheringTester(toList(TEST_IP4_ADDR),
                 toList(TEST_IP4_DNS));
         final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
@@ -549,123 +511,6 @@
         final InetAddress clientIp = tethered.ipv4Addr;
         sendUploadPacketUdp(srcMac, dstMac, clientIp, remoteIp, tester, false /* is4To6 */);
         sendDownloadPacketUdp(remoteIp, tetheringUpstreamIp, tester, false /* is6To4 */);
-
-        if (verifyBpf) {
-            // Send second UDP packet in original direction.
-            // The BPF coordinator only offloads the ASSURED conntrack entry. The "request + reply"
-            // packets can make status IPS_SEEN_REPLY to be set. Need one more packet to make
-            // conntrack status IPS_ASSURED_BIT to be set. Note the third packet needs to delay
-            // 2 seconds because kernel monitors a UDP connection which still alive after 2 seconds
-            // and apply ASSURED flag.
-            // See kernel upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5 and
-            // nf_conntrack_udp_packet in net/netfilter/nf_conntrack_proto_udp.c
-            Thread.sleep(UDP_STREAM_TS_MS);
-            sendUploadPacketUdp(srcMac, dstMac, clientIp, remoteIp, tester, false /* is4To6 */);
-
-            // Give a slack time for handling conntrack event in user space.
-            Thread.sleep(UDP_STREAM_SLACK_MS);
-
-            // [1] Verify IPv4 upstream rule map.
-            final HashMap<Tether4Key, Tether4Value> upstreamMap = pollRawMapFromDump(
-                    Tether4Key.class, Tether4Value.class, DUMPSYS_RAWMAP_ARG_UPSTREAM4);
-            assertNotNull(upstreamMap);
-            assertEquals(1, upstreamMap.size());
-
-            final Map.Entry<Tether4Key, Tether4Value> rule =
-                    upstreamMap.entrySet().iterator().next();
-
-            final Tether4Key upstream4Key = rule.getKey();
-            assertEquals(IPPROTO_UDP, upstream4Key.l4proto);
-            assertTrue(Arrays.equals(tethered.ipv4Addr.getAddress(), upstream4Key.src4));
-            assertEquals(LOCAL_PORT, upstream4Key.srcPort);
-            assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(), upstream4Key.dst4));
-            assertEquals(REMOTE_PORT, upstream4Key.dstPort);
-
-            final Tether4Value upstream4Value = rule.getValue();
-            assertTrue(Arrays.equals(tetheringUpstreamIp.getAddress(),
-                    InetAddress.getByAddress(upstream4Value.src46).getAddress()));
-            assertEquals(LOCAL_PORT, upstream4Value.srcPort);
-            assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(),
-                    InetAddress.getByAddress(upstream4Value.dst46).getAddress()));
-            assertEquals(REMOTE_PORT, upstream4Value.dstPort);
-
-            // [2] Verify stats map.
-            // Transmit packets on both direction for verifying stats. Because we only care the
-            // packet count in stats test, we just reuse the existing packets to increaes
-            // the packet count on both direction.
-
-            // Send packets on original direction.
-            for (int i = 0; i < TX_UDP_PACKET_COUNT; i++) {
-                sendUploadPacketUdp(srcMac, dstMac, clientIp, remoteIp, tester,
-                        false /* is4To6 */);
-            }
-
-            // Send packets on reply direction.
-            for (int i = 0; i < RX_UDP_PACKET_COUNT; i++) {
-                sendDownloadPacketUdp(remoteIp, tetheringUpstreamIp, tester, false /* is6To4 */);
-            }
-
-            // Dump stats map to verify.
-            final HashMap<TetherStatsKey, TetherStatsValue> statsMap = pollRawMapFromDump(
-                    TetherStatsKey.class, TetherStatsValue.class, DUMPSYS_RAWMAP_ARG_STATS);
-            assertNotNull(statsMap);
-            assertEquals(1, statsMap.size());
-
-            final Map.Entry<TetherStatsKey, TetherStatsValue> stats =
-                    statsMap.entrySet().iterator().next();
-
-            // TODO: verify the upstream index in TetherStatsKey.
-
-            final TetherStatsValue statsValue = stats.getValue();
-            assertEquals(RX_UDP_PACKET_COUNT, statsValue.rxPackets);
-            assertEquals(RX_UDP_PACKET_COUNT * RX_UDP_PACKET_SIZE, statsValue.rxBytes);
-            assertEquals(0, statsValue.rxErrors);
-            assertEquals(TX_UDP_PACKET_COUNT, statsValue.txPackets);
-            assertEquals(TX_UDP_PACKET_COUNT * TX_UDP_PACKET_SIZE, statsValue.txBytes);
-            assertEquals(0, statsValue.txErrors);
-        }
-    }
-
-    private static boolean isUdpOffloadSupportedByKernel(final String kernelVersion) {
-        final KVersion current = DeviceInfoUtils.getMajorMinorSubminorVersion(kernelVersion);
-        return current.isInRange(new KVersion(4, 14, 222), new KVersion(4, 19, 0))
-                || current.isInRange(new KVersion(4, 19, 176), new KVersion(5, 4, 0))
-                || current.isAtLeast(new KVersion(5, 4, 98));
-    }
-
-    @Test
-    public void testIsUdpOffloadSupportedByKernel() throws Exception {
-        assertFalse(isUdpOffloadSupportedByKernel("4.14.221"));
-        assertTrue(isUdpOffloadSupportedByKernel("4.14.222"));
-        assertTrue(isUdpOffloadSupportedByKernel("4.16.0"));
-        assertTrue(isUdpOffloadSupportedByKernel("4.18.0"));
-        assertFalse(isUdpOffloadSupportedByKernel("4.19.0"));
-
-        assertFalse(isUdpOffloadSupportedByKernel("4.19.175"));
-        assertTrue(isUdpOffloadSupportedByKernel("4.19.176"));
-        assertTrue(isUdpOffloadSupportedByKernel("5.2.0"));
-        assertTrue(isUdpOffloadSupportedByKernel("5.3.0"));
-        assertFalse(isUdpOffloadSupportedByKernel("5.4.0"));
-
-        assertFalse(isUdpOffloadSupportedByKernel("5.4.97"));
-        assertTrue(isUdpOffloadSupportedByKernel("5.4.98"));
-        assertTrue(isUdpOffloadSupportedByKernel("5.10.0"));
-    }
-
-    private static void assumeKernelSupportBpfOffloadUdpV4() {
-        final String kernelVersion = VintfRuntimeInfo.getKernelVersion();
-        assumeTrue("Kernel version " + kernelVersion + " doesn't support IPv4 UDP BPF offload",
-                isUdpOffloadSupportedByKernel(kernelVersion));
-    }
-
-    @Test
-    public void testKernelSupportBpfOffloadUdpV4() throws Exception {
-        assumeKernelSupportBpfOffloadUdpV4();
-    }
-
-    @Test
-    public void testTetherConfigBpfOffloadEnabled() throws Exception {
-        assumeTrue(isTetherConfigBpfOffloadEnabled());
     }
 
     /**
@@ -674,73 +519,7 @@
      */
     @Test
     public void testTetherUdpV4() throws Exception {
-        runUdp4Test(false /* verifyBpf */);
-    }
-
-    /**
-     * BPF offload IPv4 UDP tethering test. Verify that UDP tethered packets are offloaded by BPF.
-     * Minimum test requirement:
-     * 1. S+ device.
-     * 2. Tethering config enables tethering BPF offload.
-     * 3. Kernel supports IPv4 UDP BPF offload. See #isUdpOffloadSupportedByKernel.
-     *
-     * TODO: consider enabling the test even tethering config disables BPF offload. See b/238288883
-     */
-    @Test
-    @IgnoreUpTo(Build.VERSION_CODES.R)
-    public void testTetherUdpV4_VerifyBpf() throws Exception {
-        assumeTrue("Tethering config disabled BPF offload", isTetherConfigBpfOffloadEnabled());
-        assumeKernelSupportBpfOffloadUdpV4();
-
-        runUdp4Test(true /* verifyBpf */);
-    }
-
-    @NonNull
-    private <K extends Struct, V extends Struct> HashMap<K, V> dumpAndParseRawMap(
-            Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
-            throws Exception {
-        final String[] args = new String[] {DUMPSYS_TETHERING_RAWMAP_ARG, mapArg};
-        final String rawMapStr = runAsShell(DUMP, () ->
-                DumpTestUtils.dumpService(Context.TETHERING_SERVICE, args));
-        final HashMap<K, V> map = new HashMap<>();
-
-        for (final String line : rawMapStr.split(LINE_DELIMITER)) {
-            final Pair<K, V> rule =
-                    BpfDump.fromBase64EncodedString(keyClass, valueClass, line.trim());
-            map.put(rule.first, rule.second);
-        }
-        return map;
-    }
-
-    @Nullable
-    private <K extends Struct, V extends Struct> HashMap<K, V> pollRawMapFromDump(
-            Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
-            throws Exception {
-        for (int retryCount = 0; retryCount < DUMP_POLLING_MAX_RETRY; retryCount++) {
-            final HashMap<K, V> map = dumpAndParseRawMap(keyClass, valueClass, mapArg);
-            if (!map.isEmpty()) return map;
-
-            Thread.sleep(DUMP_POLLING_INTERVAL_MS);
-        }
-
-        fail("Cannot get rules after " + DUMP_POLLING_MAX_RETRY * DUMP_POLLING_INTERVAL_MS + "ms");
-        return null;
-    }
-
-    private boolean isTetherConfigBpfOffloadEnabled() throws Exception {
-        final String dumpStr = runAsShell(DUMP, () ->
-                DumpTestUtils.dumpService(Context.TETHERING_SERVICE, "--short"));
-
-        // BPF offload tether config can be overridden by "config_tether_enable_bpf_offload" in
-        // packages/modules/Connectivity/Tethering/res/values/config.xml. OEM may disable config by
-        // RRO to override the enabled default value. Get the tethering config via dumpsys.
-        // $ dumpsys tethering
-        //   mIsBpfEnabled: true
-        boolean enabled = dumpStr.contains("mIsBpfEnabled: true");
-        if (!enabled) {
-            Log.d(TAG, "BPF offload tether config not enabled: " + dumpStr);
-        }
-        return enabled;
+        runUdp4Test();
     }
 
     @NonNull
diff --git a/Tethering/tests/mts/Android.bp b/Tethering/tests/mts/Android.bp
index ae36499..4f4b03c 100644
--- a/Tethering/tests/mts/Android.bp
+++ b/Tethering/tests/mts/Android.bp
@@ -33,6 +33,7 @@
     ],
 
     static_libs: [
+        "TetheringIntegrationTestsBaseLib",
         "androidx.test.rules",
         // mockito-target-extended-minus-junit4 used in this lib have dependency with
         // jni_libs libdexmakerjvmtiagent and libstaticjvmtiagent.
diff --git a/Tethering/tests/mts/src/android/tethering/mts/MtsEthernetTetheringTest.java b/Tethering/tests/mts/src/android/tethering/mts/MtsEthernetTetheringTest.java
new file mode 100644
index 0000000..cb57d13
--- /dev/null
+++ b/Tethering/tests/mts/src/android/tethering/mts/MtsEthernetTetheringTest.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static android.Manifest.permission.DUMP;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import static com.android.testutils.DeviceInfoUtils.KVersion;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.net.TetheringTester.TetheredDevice;
+import android.os.Build;
+import android.os.VintfRuntimeInfo;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.BpfDump;
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.bpf.Tether4Key;
+import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsKey;
+import com.android.net.module.util.bpf.TetherStatsValue;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DeviceInfoUtils;
+import com.android.testutils.DumpTestUtils;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class MtsEthernetTetheringTest extends EthernetTetheringTestBase {
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
+    private static final String TAG = MtsEthernetTetheringTest.class.getSimpleName();
+
+    private static final int DUMP_POLLING_MAX_RETRY = 100;
+    private static final int DUMP_POLLING_INTERVAL_MS = 50;
+    // Kernel treats a confirmed UDP connection which active after two seconds as stream mode.
+    // See upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5.
+    private static final int UDP_STREAM_TS_MS = 2000;
+    // Give slack time for waiting UDP stream mode because handling conntrack event in user space
+    // may not in precise time. Used to reduce the flaky rate.
+    private static final int UDP_STREAM_SLACK_MS = 500;
+    // Per RX UDP packet size: iphdr (20) + udphdr (8) + payload (2) = 30 bytes.
+    private static final int RX_UDP_PACKET_SIZE = 30;
+    private static final int RX_UDP_PACKET_COUNT = 456;
+    // Per TX UDP packet size: ethhdr (14) + iphdr (20) + udphdr (8) + payload (2) = 44 bytes.
+    private static final int TX_UDP_PACKET_SIZE = 44;
+    private static final int TX_UDP_PACKET_COUNT = 123;
+
+    private static final String DUMPSYS_TETHERING_RAWMAP_ARG = "bpfRawMap";
+    private static final String DUMPSYS_RAWMAP_ARG_STATS = "--stats";
+    private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
+    private static final String LINE_DELIMITER = "\\n";
+
+    private static boolean isUdpOffloadSupportedByKernel(final String kernelVersion) {
+        final KVersion current = DeviceInfoUtils.getMajorMinorSubminorVersion(kernelVersion);
+        return current.isInRange(new KVersion(4, 14, 222), new KVersion(4, 19, 0))
+                || current.isInRange(new KVersion(4, 19, 176), new KVersion(5, 4, 0))
+                || current.isAtLeast(new KVersion(5, 4, 98));
+    }
+
+    @Test
+    public void testIsUdpOffloadSupportedByKernel() throws Exception {
+        assertFalse(isUdpOffloadSupportedByKernel("4.14.221"));
+        assertTrue(isUdpOffloadSupportedByKernel("4.14.222"));
+        assertTrue(isUdpOffloadSupportedByKernel("4.16.0"));
+        assertTrue(isUdpOffloadSupportedByKernel("4.18.0"));
+        assertFalse(isUdpOffloadSupportedByKernel("4.19.0"));
+
+        assertFalse(isUdpOffloadSupportedByKernel("4.19.175"));
+        assertTrue(isUdpOffloadSupportedByKernel("4.19.176"));
+        assertTrue(isUdpOffloadSupportedByKernel("5.2.0"));
+        assertTrue(isUdpOffloadSupportedByKernel("5.3.0"));
+        assertFalse(isUdpOffloadSupportedByKernel("5.4.0"));
+
+        assertFalse(isUdpOffloadSupportedByKernel("5.4.97"));
+        assertTrue(isUdpOffloadSupportedByKernel("5.4.98"));
+        assertTrue(isUdpOffloadSupportedByKernel("5.10.0"));
+    }
+
+    private static void assumeKernelSupportBpfOffloadUdpV4() {
+        final String kernelVersion = VintfRuntimeInfo.getKernelVersion();
+        assumeTrue("Kernel version " + kernelVersion + " doesn't support IPv4 UDP BPF offload",
+                isUdpOffloadSupportedByKernel(kernelVersion));
+    }
+
+    @Test
+    public void testKernelSupportBpfOffloadUdpV4() throws Exception {
+        assumeKernelSupportBpfOffloadUdpV4();
+    }
+
+    private boolean isTetherConfigBpfOffloadEnabled() throws Exception {
+        final String dumpStr = runAsShell(DUMP, () ->
+                DumpTestUtils.dumpService(Context.TETHERING_SERVICE, "--short"));
+
+        // BPF offload tether config can be overridden by "config_tether_enable_bpf_offload" in
+        // packages/modules/Connectivity/Tethering/res/values/config.xml. OEM may disable config by
+        // RRO to override the enabled default value. Get the tethering config via dumpsys.
+        // $ dumpsys tethering
+        //   mIsBpfEnabled: true
+        boolean enabled = dumpStr.contains("mIsBpfEnabled: true");
+        if (!enabled) {
+            Log.d(TAG, "BPF offload tether config not enabled: " + dumpStr);
+        }
+        return enabled;
+    }
+
+    @Test
+    public void testTetherConfigBpfOffloadEnabled() throws Exception {
+        assumeTrue(isTetherConfigBpfOffloadEnabled());
+    }
+
+    @NonNull
+    private <K extends Struct, V extends Struct> HashMap<K, V> dumpAndParseRawMap(
+            Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
+            throws Exception {
+        final String[] args = new String[] {DUMPSYS_TETHERING_RAWMAP_ARG, mapArg};
+        final String rawMapStr = runAsShell(DUMP, () ->
+                DumpTestUtils.dumpService(Context.TETHERING_SERVICE, args));
+        final HashMap<K, V> map = new HashMap<>();
+
+        for (final String line : rawMapStr.split(LINE_DELIMITER)) {
+            final Pair<K, V> rule =
+                    BpfDump.fromBase64EncodedString(keyClass, valueClass, line.trim());
+            map.put(rule.first, rule.second);
+        }
+        return map;
+    }
+
+    @Nullable
+    private <K extends Struct, V extends Struct> HashMap<K, V> pollRawMapFromDump(
+            Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
+            throws Exception {
+        for (int retryCount = 0; retryCount < DUMP_POLLING_MAX_RETRY; retryCount++) {
+            final HashMap<K, V> map = dumpAndParseRawMap(keyClass, valueClass, mapArg);
+            if (!map.isEmpty()) return map;
+
+            Thread.sleep(DUMP_POLLING_INTERVAL_MS);
+        }
+
+        fail("Cannot get rules after " + DUMP_POLLING_MAX_RETRY * DUMP_POLLING_INTERVAL_MS + "ms");
+        return null;
+    }
+
+    // Test network topology:
+    //
+    //         public network (rawip)                 private network
+    //                   |                 UE                |
+    // +------------+    V    +------------+------------+    V    +------------+
+    // |   Sever    +---------+  Upstream  | Downstream +---------+   Client   |
+    // +------------+         +------------+------------+         +------------+
+    // remote ip              public ip                           private ip
+    // 8.8.8.8:443            <Upstream ip>:9876                  <TetheredDevice ip>:9876
+    //
+    private void runUdp4Test() throws Exception {
+        final TetheringTester tester = initTetheringTester(toList(TEST_IP4_ADDR),
+                toList(TEST_IP4_DNS));
+        final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
+
+        // TODO: remove the connectivity verification for upstream connected notification race.
+        // Because async upstream connected notification can't guarantee the tethering routing is
+        // ready to use. Need to test tethering connectivity before testing.
+        // For short term plan, consider using IPv6 RA to get MAC address because the prefix comes
+        // from upstream. That can guarantee that the routing is ready. Long term plan is that
+        // refactors upstream connected notification from async to sync.
+        probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
+
+        final MacAddress srcMac = tethered.macAddr;
+        final MacAddress dstMac = tethered.routerMacAddr;
+        final InetAddress remoteIp = REMOTE_IP4_ADDR;
+        final InetAddress tetheringUpstreamIp = TEST_IP4_ADDR.getAddress();
+        final InetAddress clientIp = tethered.ipv4Addr;
+        sendUploadPacketUdp(srcMac, dstMac, clientIp, remoteIp, tester, false /* is4To6 */);
+        sendDownloadPacketUdp(remoteIp, tetheringUpstreamIp, tester, false /* is6To4 */);
+
+        // Send second UDP packet in original direction.
+        // The BPF coordinator only offloads the ASSURED conntrack entry. The "request + reply"
+        // packets can make status IPS_SEEN_REPLY to be set. Need one more packet to make
+        // conntrack status IPS_ASSURED_BIT to be set. Note the third packet needs to delay
+        // 2 seconds because kernel monitors a UDP connection which still alive after 2 seconds
+        // and apply ASSURED flag.
+        // See kernel upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5 and
+        // nf_conntrack_udp_packet in net/netfilter/nf_conntrack_proto_udp.c
+        Thread.sleep(UDP_STREAM_TS_MS);
+        sendUploadPacketUdp(srcMac, dstMac, clientIp, remoteIp, tester, false /* is4To6 */);
+
+        // Give a slack time for handling conntrack event in user space.
+        Thread.sleep(UDP_STREAM_SLACK_MS);
+
+        // [1] Verify IPv4 upstream rule map.
+        final HashMap<Tether4Key, Tether4Value> upstreamMap = pollRawMapFromDump(
+                Tether4Key.class, Tether4Value.class, DUMPSYS_RAWMAP_ARG_UPSTREAM4);
+        assertNotNull(upstreamMap);
+        assertEquals(1, upstreamMap.size());
+
+        final Map.Entry<Tether4Key, Tether4Value> rule =
+                upstreamMap.entrySet().iterator().next();
+
+        final Tether4Key upstream4Key = rule.getKey();
+        assertEquals(IPPROTO_UDP, upstream4Key.l4proto);
+        assertTrue(Arrays.equals(tethered.ipv4Addr.getAddress(), upstream4Key.src4));
+        assertEquals(LOCAL_PORT, upstream4Key.srcPort);
+        assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(), upstream4Key.dst4));
+        assertEquals(REMOTE_PORT, upstream4Key.dstPort);
+
+        final Tether4Value upstream4Value = rule.getValue();
+        assertTrue(Arrays.equals(tetheringUpstreamIp.getAddress(),
+                InetAddress.getByAddress(upstream4Value.src46).getAddress()));
+        assertEquals(LOCAL_PORT, upstream4Value.srcPort);
+        assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(),
+                InetAddress.getByAddress(upstream4Value.dst46).getAddress()));
+        assertEquals(REMOTE_PORT, upstream4Value.dstPort);
+
+        // [2] Verify stats map.
+        // Transmit packets on both direction for verifying stats. Because we only care the
+        // packet count in stats test, we just reuse the existing packets to increaes
+        // the packet count on both direction.
+
+        // Send packets on original direction.
+        for (int i = 0; i < TX_UDP_PACKET_COUNT; i++) {
+            sendUploadPacketUdp(srcMac, dstMac, clientIp, remoteIp, tester,
+                    false /* is4To6 */);
+        }
+
+        // Send packets on reply direction.
+        for (int i = 0; i < RX_UDP_PACKET_COUNT; i++) {
+            sendDownloadPacketUdp(remoteIp, tetheringUpstreamIp, tester, false /* is6To4 */);
+        }
+
+        // Dump stats map to verify.
+        final HashMap<TetherStatsKey, TetherStatsValue> statsMap = pollRawMapFromDump(
+                TetherStatsKey.class, TetherStatsValue.class, DUMPSYS_RAWMAP_ARG_STATS);
+        assertNotNull(statsMap);
+        assertEquals(1, statsMap.size());
+
+        final Map.Entry<TetherStatsKey, TetherStatsValue> stats =
+                statsMap.entrySet().iterator().next();
+
+        // TODO: verify the upstream index in TetherStatsKey.
+
+        final TetherStatsValue statsValue = stats.getValue();
+        assertEquals(RX_UDP_PACKET_COUNT, statsValue.rxPackets);
+        assertEquals(RX_UDP_PACKET_COUNT * RX_UDP_PACKET_SIZE, statsValue.rxBytes);
+        assertEquals(0, statsValue.rxErrors);
+        assertEquals(TX_UDP_PACKET_COUNT, statsValue.txPackets);
+        assertEquals(TX_UDP_PACKET_COUNT * TX_UDP_PACKET_SIZE, statsValue.txBytes);
+        assertEquals(0, statsValue.txErrors);
+    }
+
+    /**
+     * BPF offload IPv4 UDP tethering test. Verify that UDP tethered packets are offloaded by BPF.
+     * Minimum test requirement:
+     * 1. S+ device.
+     * 2. Tethering config enables tethering BPF offload.
+     * 3. Kernel supports IPv4 UDP BPF offload. See #isUdpOffloadSupportedByKernel.
+     *
+     * TODO: consider enabling the test even tethering config disables BPF offload. See b/238288883
+     */
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testTetherBpfOffloadUdpV4() throws Exception {
+        assumeTrue("Tethering config disabled BPF offload", isTetherConfigBpfOffloadEnabled());
+        assumeKernelSupportBpfOffloadUdpV4();
+
+        runUdp4Test();
+    }
+}
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index f623b05..193bd92 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -15,7 +15,7 @@
     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 @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.CONNECTIVITY_USE_RESTRICTED_NETWORKS}) public void registerSystemDefaultNetworkCallback(@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);
     method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void removeUidFromMeteredNetworkDenyList(int);
     method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void replaceFirewallChain(int, @NonNull int[]);
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index 0b03983..196e023 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -470,7 +470,7 @@
   }
 
   public abstract class SocketKeepalive implements java.lang.AutoCloseable {
-    method public final void start(@IntRange(from=0xa, to=0xe10) int, int);
+    method public final void start(@IntRange(from=0xa, to=0xe10) int, int, @NonNull android.net.Network);
     field public static final int ERROR_NO_SUCH_SLOT = -33; // 0xffffffdf
     field public static final int FLAG_AUTOMATIC_ON_OFF = 1; // 0x1
     field public static final int SUCCESS = 0; // 0x0
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 79c94ab..7cef58b 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -4901,6 +4901,7 @@
     @RequiresPermission(anyOf = {
             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 NetworkCallback networkCallback,
             @NonNull Handler handler) {
@@ -6035,6 +6036,30 @@
     }
 
     /**
+     * Get firewall rule of specified firewall chain on specified uid.
+     *
+     * @param chain target chain.
+     * @param uid   target uid
+     * @return either FIREWALL_RULE_ALLOW or FIREWALL_RULE_DENY
+     * @throws UnsupportedOperationException if called on pre-T devices.
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *                                  cause of the failure.
+     * @hide
+     */
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_STACK,
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
+    })
+    public int getUidFirewallRule(@FirewallChain final int chain, final int uid) {
+        try {
+            return mService.getUidFirewallRule(chain, uid);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Enables or disables the specified firewall chain.
      *
      * @param chain target chain.
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index c8f588d..1372e9a 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -188,7 +188,7 @@
 
     void startNattKeepaliveWithFd(in Network network, in ParcelFileDescriptor pfd, int resourceId,
             int intervalSeconds, in ISocketKeepaliveCallback cb, String srcAddr,
-            String dstAddr, boolean automaticOnOffKeepalives);
+            String dstAddr, boolean automaticOnOffKeepalives, in Network underpinnedNetwork);
 
     void startTcpKeepalive(in Network network, in ParcelFileDescriptor pfd, int intervalSeconds,
             in ISocketKeepaliveCallback cb);
@@ -242,6 +242,8 @@
 
     void setUidFirewallRule(int chain, int uid, int rule);
 
+    int getUidFirewallRule(int chain, int uid);
+
     void setFirewallChainEnabled(int chain, boolean enable);
 
     boolean getFirewallChainEnabled(int chain);
diff --git a/framework/src/android/net/NattSocketKeepalive.java b/framework/src/android/net/NattSocketKeepalive.java
index 77137f4..a83b5b4 100644
--- a/framework/src/android/net/NattSocketKeepalive.java
+++ b/framework/src/android/net/NattSocketKeepalive.java
@@ -66,10 +66,12 @@
      *                    the supplied {@link Callback} will see a call to
      *                    {@link Callback#onError(int)} with {@link #ERROR_INVALID_INTERVAL}.
      * @param flags Flags to enable/disable available options on this keepalive.
+     * @param underpinnedNetwork The underpinned network of this keepalive.
+     *
      * @hide
      */
     @Override
-    protected void startImpl(int intervalSec, int flags) {
+    protected void startImpl(int intervalSec, int flags, Network underpinnedNetwork) {
         if (0 != (flags & ~FLAG_AUTOMATIC_ON_OFF)) {
             throw new IllegalArgumentException("Illegal flag value for "
                     + this.getClass().getSimpleName() + " : " + flags);
@@ -79,7 +81,8 @@
             try {
                 mService.startNattKeepaliveWithFd(mNetwork, mPfd, mResourceId,
                         intervalSec, mCallback, mSource.getHostAddress(),
-                        mDestination.getHostAddress(), automaticOnOffKeepalives);
+                        mDestination.getHostAddress(), automaticOnOffKeepalives,
+                        underpinnedNetwork);
             } catch (RemoteException e) {
                 Log.e(TAG, "Error starting socket keepalive: ", e);
                 throw e.rethrowFromSystemServer();
diff --git a/framework/src/android/net/SocketKeepalive.java b/framework/src/android/net/SocketKeepalive.java
index 00104f6..311126e 100644
--- a/framework/src/android/net/SocketKeepalive.java
+++ b/framework/src/android/net/SocketKeepalive.java
@@ -355,7 +355,7 @@
      */
     public final void start(@IntRange(from = MIN_INTERVAL_SEC, to = MAX_INTERVAL_SEC)
             int intervalSec) {
-        startImpl(intervalSec, 0 /* flags */);
+        startImpl(intervalSec, 0 /* flags */, null /* underpinnedNetwork */);
     }
 
     /**
@@ -374,16 +374,18 @@
      *                    the supplied {@link Callback} will see a call to
      *                    {@link Callback#onError(int)} with {@link #ERROR_INVALID_INTERVAL}.
      * @param flags Flags to enable/disable available options on this keepalive.
+     * @param underpinnedNetwork The underpinned network of this keepalive.
      * @hide
      */
     @SystemApi(client = PRIVILEGED_APPS)
     public final void start(@IntRange(from = MIN_INTERVAL_SEC, to = MAX_INTERVAL_SEC)
-            int intervalSec, @StartFlags int flags) {
-        startImpl(intervalSec, flags);
+            int intervalSec, @StartFlags int flags, @NonNull Network underpinnedNetwork) {
+        startImpl(intervalSec, flags, underpinnedNetwork);
     }
 
     /** @hide */
-    protected abstract void startImpl(int intervalSec, @StartFlags int flags);
+    protected abstract void startImpl(int intervalSec, @StartFlags int flags,
+            Network underpinnedNetwork);
 
     /**
      * Requests that keepalive be stopped. The application must wait for {@link Callback#onStopped}
diff --git a/framework/src/android/net/TcpSocketKeepalive.java b/framework/src/android/net/TcpSocketKeepalive.java
index cda5830..b548f6d 100644
--- a/framework/src/android/net/TcpSocketKeepalive.java
+++ b/framework/src/android/net/TcpSocketKeepalive.java
@@ -50,7 +50,7 @@
      *   acknowledgement.
      */
     @Override
-    protected void startImpl(int intervalSec, int flags) {
+    protected void startImpl(int intervalSec, int flags, Network underpinnedNetwork) {
         if (0 != flags) {
             throw new IllegalArgumentException("Illegal flag value for "
                     + this.getClass().getSimpleName() + " : " + flags);
diff --git a/service-t/jni/com_android_server_net_NetworkStatsFactory.cpp b/service-t/jni/com_android_server_net_NetworkStatsFactory.cpp
index a3299a7..2dbe771 100644
--- a/service-t/jni/com_android_server_net_NetworkStatsFactory.cpp
+++ b/service-t/jni/com_android_server_net_NetworkStatsFactory.cpp
@@ -210,7 +210,8 @@
 };
 
 int register_android_server_net_NetworkStatsFactory(JNIEnv* env) {
-    int err = jniRegisterNativeMethods(env, "com/android/server/net/NetworkStatsFactory", gMethods,
+    int err = jniRegisterNativeMethods(env,
+            "android/net/connectivity/com/android/server/net/NetworkStatsFactory", gMethods,
             NELEM(gMethods));
     gStringClass = env->FindClass("java/lang/String");
     gStringClass = static_cast<jclass>(env->NewGlobalRef(gStringClass));
diff --git a/service-t/jni/com_android_server_net_NetworkStatsService.cpp b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
index af0b8d8..dab9d07 100644
--- a/service-t/jni/com_android_server_net_NetworkStatsService.cpp
+++ b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
@@ -116,8 +116,9 @@
 };
 
 int register_android_server_net_NetworkStatsService(JNIEnv* env) {
-    return jniRegisterNativeMethods(env, "com/android/server/net/NetworkStatsService", gMethods,
-                                    NELEM(gMethods));
+    return jniRegisterNativeMethods(env,
+            "android/net/connectivity/com/android/server/net/NetworkStatsService", gMethods,
+            NELEM(gMethods));
 }
 
 }
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
index 60485f1..eed9aeb 100644
--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -469,9 +469,7 @@
             // TODO: Update this logic to only do a restart if required. Although a restart may
             //  be required due to the capabilities or ipConfiguration values, not all
             //  capabilities changes require a restart.
-            if (mIpClient != null) {
-                restart();
-            }
+            maybeRestart();
         }
 
         boolean isRestricted() {
@@ -549,7 +547,7 @@
                 // Send a callback in case a provisioning request was in progress.
                 return;
             }
-            restart();
+            maybeRestart();
         }
 
         private void ensureRunningOnEthernetHandlerThread() {
@@ -582,7 +580,7 @@
             // If there is a better network, that will become default and apps
             // will be able to use internet. If ethernet gets connected again,
             // and has backhaul connectivity, it will become default.
-            restart();
+            maybeRestart();
         }
 
         /** Returns true if state has been modified */
@@ -656,18 +654,16 @@
                         .build();
         }
 
-        void restart() {
-            if (DBG) Log.d(TAG, "restart IpClient");
-
+        void maybeRestart() {
             if (mIpClient == null) {
-                // If restart() is called from a provisioning failure, it is
+                // If maybeRestart() is called from a provisioning failure, it is
                 // possible that link disappeared in the meantime. In that
                 // case, stop() has already been called and IpClient should not
                 // get restarted to prevent a provisioning failure loop.
-                Log.i(TAG, String.format("restart() was called on stopped interface %s", name));
+                Log.i(TAG, String.format("maybeRestart() called on stopped interface %s", name));
                 return;
             }
-
+            if (DBG) Log.d(TAG, "restart IpClient");
             stop();
             start();
         }
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 1606fd0..2346244 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -2423,20 +2423,41 @@
         xtTotal = mXtRecorder.getTotalSinceBootLocked(template);
         uidTotal = mUidRecorder.getTotalSinceBootLocked(template);
 
-        EventLog.writeEvent(LOG_TAG_NETSTATS_MOBILE_SAMPLE,
-                xtTotal.rxBytes, xtTotal.rxPackets, xtTotal.txBytes, xtTotal.txPackets,
-                uidTotal.rxBytes, uidTotal.rxPackets, uidTotal.txBytes, uidTotal.txPackets,
-                currentTime);
+        if (SdkLevel.isAtLeastU()) {
+            EventLog.writeEvent(LOG_TAG_NETSTATS_MOBILE_SAMPLE,
+                    xtTotal.rxBytes, xtTotal.rxPackets, xtTotal.txBytes, xtTotal.txPackets,
+                    uidTotal.rxBytes, uidTotal.rxPackets, uidTotal.txBytes, uidTotal.txPackets,
+                    currentTime);
+        } else {
+            // To keep the format of event log, here replaces the value of DevRecorder with the
+            // value of XtRecorder because they have the same content in old design.
+            EventLog.writeEvent(LOG_TAG_NETSTATS_MOBILE_SAMPLE,
+                    xtTotal.rxBytes, xtTotal.rxPackets, xtTotal.txBytes, xtTotal.txPackets,
+                    xtTotal.rxBytes, xtTotal.rxPackets, xtTotal.txBytes, xtTotal.txPackets,
+                    uidTotal.rxBytes, uidTotal.rxPackets, uidTotal.txBytes, uidTotal.txPackets,
+                    currentTime);
+        }
 
         // collect wifi sample
         template = new NetworkTemplate.Builder(MATCH_WIFI).build();
         xtTotal = mXtRecorder.getTotalSinceBootLocked(template);
         uidTotal = mUidRecorder.getTotalSinceBootLocked(template);
 
-        EventLog.writeEvent(LOG_TAG_NETSTATS_WIFI_SAMPLE,
-                xtTotal.rxBytes, xtTotal.rxPackets, xtTotal.txBytes, xtTotal.txPackets,
-                uidTotal.rxBytes, uidTotal.rxPackets, uidTotal.txBytes, uidTotal.txPackets,
-                currentTime);
+        if (SdkLevel.isAtLeastU()) {
+            EventLog.writeEvent(LOG_TAG_NETSTATS_WIFI_SAMPLE,
+                    xtTotal.rxBytes, xtTotal.rxPackets, xtTotal.txBytes, xtTotal.txPackets,
+                    uidTotal.rxBytes, uidTotal.rxPackets, uidTotal.txBytes, uidTotal.txPackets,
+                    currentTime);
+        } else {
+            // To keep the format of event log, here replaces the value of DevRecorder with the
+            // value of XtRecorder because they have the same content in old design.
+            EventLog.writeEvent(LOG_TAG_NETSTATS_WIFI_SAMPLE,
+                    xtTotal.rxBytes, xtTotal.rxPackets, xtTotal.txBytes, xtTotal.txPackets,
+                    xtTotal.rxBytes, xtTotal.rxPackets, xtTotal.txBytes, xtTotal.txPackets,
+                    uidTotal.rxBytes, uidTotal.rxPackets, uidTotal.txBytes, uidTotal.txPackets,
+                    currentTime);
+
+        }
     }
 
     // deleteKernelTagData can ignore ENOENT; otherwise we should log an error
diff --git a/service/jarjar-excludes.txt b/service/jarjar-excludes.txt
index b0d6763..7bd3862 100644
--- a/service/jarjar-excludes.txt
+++ b/service/jarjar-excludes.txt
@@ -1,9 +1,3 @@
 # Classes loaded by SystemServer via their hardcoded name, so they can't be jarjared
 com\.android\.server\.ConnectivityServiceInitializer(\$.+)?
 com\.android\.server\.NetworkStatsServiceInitializer(\$.+)?
-
-# Do not jarjar com.android.server, as several unit tests fail because they lose
-# package-private visibility between jarjared and non-jarjared classes.
-# TODO: fix the tests and also jarjar com.android.server, or at least only exclude a package that
-# is specific to the module like com.android.server.connectivity
-com\.android\.server\..+
diff --git a/service/jni/com_android_server_BpfNetMaps.cpp b/service/jni/com_android_server_BpfNetMaps.cpp
index 05f50b0..77cffda 100644
--- a/service/jni/com_android_server_BpfNetMaps.cpp
+++ b/service/jni/com_android_server_BpfNetMaps.cpp
@@ -236,7 +236,7 @@
 // clang-format on
 
 int register_com_android_server_BpfNetMaps(JNIEnv* env) {
-    return jniRegisterNativeMethods(env, "com/android/server/BpfNetMaps",
+    return jniRegisterNativeMethods(env, "android/net/connectivity/com/android/server/BpfNetMaps",
                                     gMethods, NELEM(gMethods));
 }
 
diff --git a/service/jni/com_android_server_TestNetworkService.cpp b/service/jni/com_android_server_TestNetworkService.cpp
index bd74d54..7aeecfa 100644
--- a/service/jni/com_android_server_TestNetworkService.cpp
+++ b/service/jni/com_android_server_TestNetworkService.cpp
@@ -151,8 +151,9 @@
 };
 
 int register_com_android_server_TestNetworkService(JNIEnv* env) {
-    return jniRegisterNativeMethods(env, "com/android/server/TestNetworkService", gMethods,
-                                    NELEM(gMethods));
+    return jniRegisterNativeMethods(env,
+            "android/net/connectivity/com/android/server/TestNetworkService", gMethods,
+            NELEM(gMethods));
 }
 
 }; // namespace android
diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
index 7060958..5b42659 100644
--- a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
+++ b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
@@ -477,8 +477,9 @@
 };
 
 int register_com_android_server_connectivity_ClatCoordinator(JNIEnv* env) {
-    return jniRegisterNativeMethods(env, "com/android/server/connectivity/ClatCoordinator",
-                                    gMethods, NELEM(gMethods));
+    return jniRegisterNativeMethods(env,
+            "android/net/connectivity/com/android/server/connectivity/ClatCoordinator",
+            gMethods, NELEM(gMethods));
 }
 
 };  // namespace android
diff --git a/service/proguard.flags b/service/proguard.flags
index 864a28b..cf25f05 100644
--- a/service/proguard.flags
+++ b/service/proguard.flags
@@ -7,7 +7,7 @@
     *;
 }
 
--keepclassmembers class com.android.server.**,android.net.**,com.android.networkstack.** {
+-keepclassmembers class android.net.**,com.android.networkstack.** {
     static final % POLICY_*;
     static final % NOTIFY_TYPE_*;
     static final % TRANSPORT_*;
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 26ec37a..b4fce37 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -721,6 +721,31 @@
     }
 
     /**
+     * Get firewall rule of specified firewall chain on specified uid.
+     *
+     * @param childChain target chain
+     * @param uid        target uid
+     * @return either FIREWALL_RULE_ALLOW or FIREWALL_RULE_DENY
+     * @throws UnsupportedOperationException if called on pre-T devices.
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *                                  cause of the failure.
+     */
+    public int getUidRule(final int childChain, final int uid) {
+        throwIfPreT("isUidChainEnabled is not available on pre-T devices");
+
+        final long match = getMatchByFirewallChain(childChain);
+        final boolean isAllowList = isFirewallAllowList(childChain);
+        try {
+            final UidOwnerValue uidMatch = sUidOwnerMap.getValue(new S32(uid));
+            final boolean isMatchEnabled = uidMatch != null && (uidMatch.rule & match) != 0;
+            return isMatchEnabled == isAllowList ? FIREWALL_RULE_ALLOW : FIREWALL_RULE_DENY;
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno,
+                    "Unable to get uid rule status: " + Os.strerror(e.errno));
+        }
+    }
+
+    /**
      * Add ingress interface filtering rules to a list of UIDs
      *
      * For a given uid, once a filtering rule is added, the kernel will only allow packets from the
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 1332f32..394292e 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -2882,9 +2882,10 @@
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
     }
 
-    private void enforceSettingsOrUseRestrictedNetworksPermission() {
+    private void enforceSettingsOrSetupWizardOrUseRestrictedNetworksPermission() {
         enforceAnyPermissionOf(mContext,
                 android.Manifest.permission.NETWORK_SETTINGS,
+                android.Manifest.permission.NETWORK_SETUP_WIZARD,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
                 Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS);
     }
@@ -5589,13 +5590,14 @@
                             mKeepaliveTracker.getKeepaliveForBinder((IBinder) msg.obj);
                     if (null == ki) return; // The callback was unregistered before the alarm fired
 
+                    final Network underpinnedNetwork = ki.getUnderpinnedNetwork();
                     final Network network = ki.getNetwork();
                     boolean networkFound = false;
-                    final ArrayList<NetworkAgentInfo> vpnsRunningOnThisNetwork = new ArrayList<>();
+                    boolean underpinnedNetworkFound = false;
                     for (NetworkAgentInfo n : mNetworkAgentInfos) {
                         if (n.network.equals(network)) networkFound = true;
-                        if (n.isVPN() && n.everConnected() && hasUnderlyingNetwork(n, network)) {
-                            vpnsRunningOnThisNetwork.add(n);
+                        if (n.everConnected() && n.network.equals(underpinnedNetwork)) {
+                            underpinnedNetworkFound = true;
                         }
                     }
 
@@ -5603,12 +5605,11 @@
                     // cleaned up already. There is no point trying to resume keepalives.
                     if (!networkFound) return;
 
-                    if (!vpnsRunningOnThisNetwork.isEmpty()) {
+                    if (underpinnedNetworkFound) {
                         mKeepaliveTracker.handleMonitorAutomaticKeepalive(ki,
-                                // TODO: check all the VPNs running on top of this network
-                                vpnsRunningOnThisNetwork.get(0).network.netId);
+                                underpinnedNetwork.netId);
                     } else {
-                        // If no VPN, then make sure the keepalive is running.
+                        // If no underpinned network, then make sure the keepalive is running.
                         mKeepaliveTracker.handleMaybeResumeKeepalive(ki);
                     }
                     break;
@@ -6848,7 +6849,7 @@
                 enforceAccessPermission();
                 break;
             case TRACK_SYSTEM_DEFAULT:
-                enforceSettingsOrUseRestrictedNetworksPermission();
+                enforceSettingsOrSetupWizardOrUseRestrictedNetworksPermission();
                 networkCapabilities = new NetworkCapabilities(defaultNc);
                 break;
             case BACKGROUND_REQUEST:
@@ -9868,21 +9869,22 @@
                 getNetworkAgentInfoForNetwork(network), null /* fd */,
                 intervalSeconds, cb, srcAddr, srcPort, dstAddr, NattSocketKeepalive.NATT_PORT,
                 // Keep behavior of the deprecated method as it is. Set automaticOnOffKeepalives to
-                // false because there is no way and no plan to configure automaticOnOffKeepalives
-                // in this deprecated method.
-                false /* automaticOnOffKeepalives */);
+                // false and set the underpinned network to null because there is no way and no
+                // plan to configure automaticOnOffKeepalives or underpinnedNetwork in this
+                // deprecated method.
+                false /* automaticOnOffKeepalives */, null /* underpinnedNetwork */);
     }
 
     @Override
     public void startNattKeepaliveWithFd(Network network, ParcelFileDescriptor pfd, int resourceId,
             int intervalSeconds, ISocketKeepaliveCallback cb, String srcAddr,
-            String dstAddr, boolean automaticOnOffKeepalives) {
+            String dstAddr, boolean automaticOnOffKeepalives, Network underpinnedNetwork) {
         try {
             final FileDescriptor fd = pfd.getFileDescriptor();
             mKeepaliveTracker.startNattKeepalive(
                     getNetworkAgentInfoForNetwork(network), fd, resourceId,
-                    intervalSeconds, cb,
-                    srcAddr, dstAddr, NattSocketKeepalive.NATT_PORT, automaticOnOffKeepalives);
+                    intervalSeconds, cb, srcAddr, dstAddr, NattSocketKeepalive.NATT_PORT,
+                    automaticOnOffKeepalives, underpinnedNetwork);
         } finally {
             // FileDescriptors coming from AIDL calls must be manually closed to prevent leaks.
             // startNattKeepalive calls Os.dup(fd) before returning, so we can close immediately.
@@ -11745,6 +11747,12 @@
         }
     }
 
+    @Override
+    public int getUidFirewallRule(final int chain, final int uid) {
+        enforceNetworkStackOrSettingsPermission();
+        return mBpfNetMaps.getUidRule(chain, uid);
+    }
+
     private int getFirewallRuleType(int chain, int rule) {
         final int defaultRule;
         switch (chain) {
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
index daabfd8..760a5f3 100644
--- a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -196,11 +196,16 @@
         private final PendingIntent mTcpPollingAlarm;
         @AutomaticOnOffState
         private int mAutomaticOnOffState;
+        @Nullable
+        private final Network mUnderpinnedNetwork;
 
         AutomaticOnOffKeepalive(@NonNull final KeepaliveTracker.KeepaliveInfo ki,
-                final boolean autoOnOff, @NonNull Context context) throws InvalidSocketException {
+                final boolean autoOnOff, @NonNull Context context,
+                @Nullable Network underpinnedNetwork)
+                throws InvalidSocketException {
             this.mKi = Objects.requireNonNull(ki);
             mCallback = ki.mCallback;
+            mUnderpinnedNetwork = underpinnedNetwork;
             if (autoOnOff && mDependencies.isFeatureEnabled(AUTOMATIC_ON_OFF_KEEPALIVE_VERSION)) {
                 mAutomaticOnOffState = STATE_ENABLED;
                 if (null == ki.mFd) {
@@ -228,6 +233,11 @@
             return mKi.getNai().network;
         }
 
+        @Nullable
+        public Network getUnderpinnedNetwork() {
+            return mUnderpinnedNetwork;
+        }
+
         public boolean match(Network network, int slot) {
             return mKi.getNai().network().equals(network) && mKi.getSlot() == slot;
         }
@@ -475,13 +485,13 @@
             @NonNull String srcAddrString,
             int srcPort,
             @NonNull String dstAddrString,
-            int dstPort, boolean automaticOnOffKeepalives) {
+            int dstPort, boolean automaticOnOffKeepalives, @Nullable Network underpinnedNetwork) {
         final KeepaliveTracker.KeepaliveInfo ki = mKeepaliveTracker.makeNattKeepaliveInfo(nai, fd,
                 intervalSeconds, cb, srcAddrString, srcPort, dstAddrString, dstPort);
         if (null == ki) return;
         try {
             final AutomaticOnOffKeepalive autoKi = new AutomaticOnOffKeepalive(ki,
-                    automaticOnOffKeepalives, mContext);
+                    automaticOnOffKeepalives, mContext, underpinnedNetwork);
             mConnectivityServiceHandler.obtainMessage(NetworkAgent.CMD_START_SOCKET_KEEPALIVE,
                     // TODO : move ConnectivityService#encodeBool to a static lib.
                     automaticOnOffKeepalives ? 1 : 0, 0, autoKi).sendToTarget();
@@ -504,13 +514,14 @@
             @NonNull String srcAddrString,
             @NonNull String dstAddrString,
             int dstPort,
-            boolean automaticOnOffKeepalives) {
+            boolean automaticOnOffKeepalives,
+            @Nullable Network underpinnedNetwork) {
         final KeepaliveTracker.KeepaliveInfo ki = mKeepaliveTracker.makeNattKeepaliveInfo(nai, fd,
                 resourceId, intervalSeconds, cb, srcAddrString, dstAddrString, dstPort);
         if (null == ki) return;
         try {
             final AutomaticOnOffKeepalive autoKi = new AutomaticOnOffKeepalive(ki,
-                    automaticOnOffKeepalives, mContext);
+                    automaticOnOffKeepalives, mContext, underpinnedNetwork);
             mConnectivityServiceHandler.obtainMessage(NetworkAgent.CMD_START_SOCKET_KEEPALIVE,
                     // TODO : move ConnectivityService#encodeBool to a static lib.
                     automaticOnOffKeepalives ? 1 : 0, 0, autoKi).sendToTarget();
@@ -539,7 +550,8 @@
         if (null == ki) return;
         try {
             final AutomaticOnOffKeepalive autoKi = new AutomaticOnOffKeepalive(ki,
-                    false /* autoOnOff, tcp keepalives are never auto on/off */, mContext);
+                    false /* autoOnOff, tcp keepalives are never auto on/off */,
+                    mContext, null /* underpinnedNetwork, tcp keepalives do not refer to this */);
             mConnectivityServiceHandler.obtainMessage(CMD_START_SOCKET_KEEPALIVE, autoKi)
                     .sendToTarget();
         } catch (InvalidSocketException e) {
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index 5c9cc63..8e47235 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -65,7 +65,7 @@
     defaults: ["jarjar-rules-combine-defaults"],
     srcs: [
         "tethering-jni-jarjar-rules.txt",
-        ":connectivity-jarjar-rules",
+        ":frameworks-net-tests-jarjar-rules",
         ":TetheringTestsJarJarRules",
         ":NetworkStackJarJarRules",
     ],
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
index d0567ae..2aa1032 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -31,6 +31,7 @@
 import com.android.tradefed.testtype.IAbi;
 import com.android.tradefed.testtype.IAbiReceiver;
 import com.android.tradefed.testtype.IBuildReceiver;
+import com.android.tradefed.util.RunUtil;
 
 import java.io.FileNotFoundException;
 import java.util.Map;
@@ -120,7 +121,7 @@
             i++;
             Log.v(TAG, "Package " + packageName + " not uninstalled yet (" + result
                     + "); sleeping 1s before polling again");
-            Thread.sleep(1000);
+            RunUtil.getDefault().sleep(1000);
         }
         fail("Package '" + packageName + "' not uinstalled after " + max_tries + " seconds");
     }
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
index 7a613b3..21c78b7 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
@@ -20,6 +20,7 @@
 
 import com.android.ddmlib.Log;
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.util.RunUtil;
 
 public class HostsideRestrictBackgroundNetworkTests extends HostsideNetworkTestCase {
 
@@ -359,7 +360,7 @@
             }
             Log.v(TAG, "whitelist check for uid " + uid + " doesn't match yet (expected "
                     + expected + ", got " + actual + "); sleeping 1s before polling again");
-            Thread.sleep(1000);
+            RunUtil.getDefault().sleep(1000);
         }
         fail("whitelist check for uid " + uid + " failed: expected "
                 + expected + ", got " + actual);
@@ -384,7 +385,7 @@
             if (result.equals(expectedResult)) return;
             Log.v(TAG, "Command '" + command + "' returned '" + result + " instead of '"
                     + expectedResult + "' on attempt #; sleeping 1s before polling again");
-            Thread.sleep(1000);
+            RunUtil.getDefault().sleep(1000);
         }
         fail("Command '" + command + "' did not return '" + expectedResult + "' after " + maxTries
                 + " attempts");
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 68b20e2..d2a3f91 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -3409,12 +3409,16 @@
         runWithShellPermissionIdentity(() -> {
             // Firewall chain status will be restored after the test.
             final boolean wasChainEnabled = mCm.getFirewallChainEnabled(chain);
+            final int previousUidFirewallRule = mCm.getUidFirewallRule(chain, myUid);
             final DatagramSocket srcSock = new DatagramSocket();
             final DatagramSocket dstSock = new DatagramSocket();
             testAndCleanup(() -> {
                 if (wasChainEnabled) {
                     mCm.setFirewallChainEnabled(chain, false /* enable */);
                 }
+                if (previousUidFirewallRule == ruleToAddMatch) {
+                    mCm.setUidFirewallRule(chain, myUid, ruleToRemoveMatch);
+                }
                 dstSock.setSoTimeout(SOCKET_TIMEOUT_MS);
 
                 // Chain disabled, UID not on chain.
@@ -3444,8 +3448,9 @@
                     // Restore the global chain status
                     mCm.setFirewallChainEnabled(chain, wasChainEnabled);
                 }, /* cleanup */ () -> {
+                    // Restore the uid firewall rule status
                     try {
-                        mCm.setUidFirewallRule(chain, myUid, ruleToRemoveMatch);
+                        mCm.setUidFirewallRule(chain, myUid, previousUidFirewallRule);
                     } catch (IllegalStateException ignored) {
                         // Removing match causes an exception when the rule entry for the uid does
                         // not exist. But this is fine and can be ignored.
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 8db307d..36b3356 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -69,6 +69,7 @@
         "java/com/android/server/VpnManagerServiceTest.java",
         "java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java",
         "java/com/android/server/connectivity/IpConnectivityMetricsTest.java",
+        "java/com/android/server/connectivity/MetricsTestUtil.java",
         "java/com/android/server/connectivity/MultipathPolicyTrackerTest.java",
         "java/com/android/server/connectivity/NetdEventListenerServiceTest.java",
         "java/com/android/server/connectivity/VpnTest.java",
@@ -136,6 +137,37 @@
     visibility: ["//packages/modules/Connectivity/tests:__subpackages__"],
 }
 
+genrule {
+    name: "frameworks-net-tests-jarjar-rules",
+    defaults: ["jarjar-rules-combine-defaults"],
+    srcs: [
+        ":frameworks-net-tests-lib-jarjar-gen",
+        // This is necessary because the tests use framework-connectivity-internal-test-defaults,
+        // which require the user to use connectivity jarjar rules.
+        ":connectivity-jarjar-rules",
+    ],
+    out: ["frameworks-net-tests-jarjar-rules.txt"],
+    visibility: ["//packages/modules/Connectivity/tests:__subpackages__"],
+}
+
+java_genrule {
+    name: "frameworks-net-tests-lib-jarjar-gen",
+    tool_files: [
+        ":FrameworksNetTestsLib{.jar}",
+        "jarjar-excludes.txt",
+    ],
+    tools: [
+        "jarjar-rules-generator",
+    ],
+    out: ["frameworks-net-tests-lib-jarjar-rules.txt"],
+    cmd: "$(location jarjar-rules-generator) " +
+        "$(location :FrameworksNetTestsLib{.jar}) " +
+        "--prefix android.net.connectivity " +
+        "--excludes $(location jarjar-excludes.txt) " +
+        "--output $(out)",
+    visibility: ["//visibility:private"],
+}
+
 android_test {
     name: "FrameworksNetTests",
     enabled: enable_frameworks_net_tests,
@@ -143,7 +175,7 @@
         "FrameworksNetTestsDefaults",
         "FrameworksNetTests-jni-defaults",
     ],
-    jarjar_rules: ":connectivity-jarjar-rules",
+    jarjar_rules: ":frameworks-net-tests-jarjar-rules",
     test_suites: ["device-tests"],
     static_libs: [
         "services.core",
diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml
index 5bac2dd..5d4bdf7 100644
--- a/tests/unit/AndroidManifest.xml
+++ b/tests/unit/AndroidManifest.xml
@@ -63,7 +63,7 @@
         <uses-library android:name="android.test.runner" />
         <uses-library android:name="android.net.ipsec.ike" />
         <activity
-            android:name="com.android.server.connectivity.NetworkNotificationManagerTest$TestDialogActivity"/>
+            android:name="android.net.connectivity.com.android.server.connectivity.NetworkNotificationManagerTest$TestDialogActivity"/>
     </application>
 
     <instrumentation
diff --git a/tests/unit/jarjar-excludes.txt b/tests/unit/jarjar-excludes.txt
new file mode 100644
index 0000000..d2022bf
--- /dev/null
+++ b/tests/unit/jarjar-excludes.txt
@@ -0,0 +1,27 @@
+# Exclude some test prefixes, otherwise the classes reference below can't find
+# them after jarjared.
+android\.compat\..+
+androidx\.test\..+
+com\.android\.frameworks\.tests\..+
+com\.android\.testutils\..+
+com\.android\.dx\.mockito\..+
+com\.android\.internal\.compat\..+
+com\.android\.internal\.org\.bouncycastle\..+
+kotlin\.test\..+
+kotlin\.reflect\..+
+org\.junit\..+
+org\.mockito\..+
+
+# Auto-jarjar-gen can't handle kotlin object expression, exclude the tests which use
+# object expressions.
+#
+# For example: Illegal class access:
+# 'android.net.connectivity.com.android.net.module.util.TrackRecordTest' attempting to access
+# 'com.android.networkstack.tethering.util.TRTInterpreter' (declaration of
+# 'android.net.connectivity.com.android.net.module.util.TrackRecordTest' ...
+#
+# In coverage test, TRTInterpreter don't be jarjar'ed to
+# android.net.connectivity* by frameworks-net-tests-jarjar-rules instead it is
+# jarjar'ed by follow up TetheringTestsJarJarRules.
+# TODO(b/269259216): remove this after fixing Auto-jarjar-gen.
+com\.android\.net\.module\.util\.TrackRecord.*
diff --git a/tests/unit/java/android/net/Ikev2VpnProfileTest.java b/tests/unit/java/android/net/Ikev2VpnProfileTest.java
index 3b68120..e12e961 100644
--- a/tests/unit/java/android/net/Ikev2VpnProfileTest.java
+++ b/tests/unit/java/android/net/Ikev2VpnProfileTest.java
@@ -492,6 +492,29 @@
     }
 
     @Test
+    public void testAutomaticNattAndIpVersionConversionIsLossless() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+        builder.setAutomaticNattKeepaliveTimerEnabled(true);
+        builder.setAutomaticIpVersionSelectionEnabled(true);
+
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        final Ikev2VpnProfile ikeProfile = builder.build();
+
+        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
+    }
+
+    @Test
+    public void testAutomaticNattAndIpVersionDefaults() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        final Ikev2VpnProfile ikeProfile = builder.build();
+
+        assertEquals(false, ikeProfile.isAutomaticNattKeepaliveTimerEnabled());
+        assertEquals(false, ikeProfile.isAutomaticIpVersionSelectionEnabled());
+    }
+
+    @Test
     public void testEquals() throws Exception {
         // Verify building without IkeTunnelConnectionParams
         final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
diff --git a/tests/unit/java/com/android/internal/net/VpnProfileTest.java b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
index 0a6d2f2..b2dff2e 100644
--- a/tests/unit/java/com/android/internal/net/VpnProfileTest.java
+++ b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
@@ -20,6 +20,7 @@
 import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS_V4;
 
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
 import static com.android.testutils.ParcelUtils.assertParcelSane;
 
 import static org.junit.Assert.assertEquals;
@@ -55,6 +56,9 @@
     private static final int ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS = 24;
     private static final int ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE = 25;
     private static final int ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION = 26;
+    private static final int ENCODED_INDEX_IKE_TUN_CONN_PARAMS = 27;
+    private static final int ENCODED_INDEX_AUTOMATIC_NATT_KEEPALIVE_TIMER_ENABLED = 28;
+    private static final int ENCODED_INDEX_AUTOMATIC_IP_VERSION_SELECTION_ENABLED = 29;
 
     @Test
     public void testDefaults() throws Exception {
@@ -85,12 +89,15 @@
         assertFalse(p.isRestrictedToTestNetworks);
         assertFalse(p.excludeLocalRoutes);
         assertFalse(p.requiresInternetValidation);
+        assertFalse(p.automaticNattKeepaliveTimerEnabled);
+        assertFalse(p.automaticIpVersionSelectionEnabled);
     }
 
     private VpnProfile getSampleIkev2Profile(String key) {
         final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */,
                 false /* excludesLocalRoutes */, true /* requiresPlatformValidation */,
-                null /* ikeTunConnParams */);
+                null /* ikeTunConnParams */, true /* mAutomaticNattKeepaliveTimerEnabled */,
+                true /* automaticIpVersionSelectionEnabled */);
 
         p.name = "foo";
         p.type = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS;
@@ -128,7 +135,9 @@
     private VpnProfile getSampleIkev2ProfileWithIkeTunConnParams(String key) {
         final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */,
                 false /* excludesLocalRoutes */, true /* requiresPlatformValidation */,
-                new IkeTunnelConnectionParams(IKE_PARAMS_V4, CHILD_PARAMS));
+                new IkeTunnelConnectionParams(IKE_PARAMS_V4, CHILD_PARAMS),
+                true /* mAutomaticNattKeepaliveTimerEnabled */,
+                true /* automaticIpVersionSelectionEnabled */);
 
         p.name = "foo";
         p.server = "bar";
@@ -166,7 +175,11 @@
 
     @Test
     public void testParcelUnparcel() {
-        if (isAtLeastT()) {
+        if (isAtLeastU()) {
+            // automaticNattKeepaliveTimerEnabled, automaticIpVersionSelectionEnabled added in U.
+            assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 28);
+            assertParcelSane(getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY), 28);
+        } else if (isAtLeastT()) {
             // excludeLocalRoutes, requiresPlatformValidation were added in T.
             assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 26);
             assertParcelSane(getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY), 26);
@@ -221,16 +234,28 @@
                         ENCODED_INDEX_AUTH_PARAMS_INLINE,
                         ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS,
                         ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE,
-                        ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION /* missingIndices */);
+                        ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION,
+                        ENCODED_INDEX_IKE_TUN_CONN_PARAMS,
+                        ENCODED_INDEX_AUTOMATIC_NATT_KEEPALIVE_TIMER_ENABLED,
+                        ENCODED_INDEX_AUTOMATIC_IP_VERSION_SELECTION_ENABLED
+                        /* missingIndices */);
 
         assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes()));
     }
 
+    private String getEncodedDecodedIkev2ProfileWithtooFewValues() {
+        return getEncodedDecodedIkev2ProfileMissingValues(
+                ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS,
+                ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE,
+                ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION,
+                ENCODED_INDEX_IKE_TUN_CONN_PARAMS,
+                ENCODED_INDEX_AUTOMATIC_NATT_KEEPALIVE_TIMER_ENABLED,
+                ENCODED_INDEX_AUTOMATIC_IP_VERSION_SELECTION_ENABLED /* missingIndices */);
+    }
+
     @Test
     public void testEncodeDecodeMissingIsRestrictedToTestNetworks() {
-        final String tooFewValues =
-                getEncodedDecodedIkev2ProfileMissingValues(
-                        ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS /* missingIndices */);
+        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
 
         // Verify decoding without isRestrictedToTestNetworks defaults to false
         final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
@@ -239,10 +264,7 @@
 
     @Test
     public void testEncodeDecodeMissingExcludeLocalRoutes() {
-        final String tooFewValues =
-                getEncodedDecodedIkev2ProfileMissingValues(
-                        ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE,
-                        ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION /* missingIndices */);
+        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
 
         // Verify decoding without excludeLocalRoutes defaults to false
         final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
@@ -251,9 +273,7 @@
 
     @Test
     public void testEncodeDecodeMissingRequiresValidation() {
-        final String tooFewValues =
-                getEncodedDecodedIkev2ProfileMissingValues(
-                        ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION /* missingIndices */);
+        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
 
         // Verify decoding without requiresValidation defaults to false
         final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
@@ -261,6 +281,24 @@
     }
 
     @Test
+    public void testEncodeDecodeMissingAutomaticNattKeepaliveTimerEnabled() {
+        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
+
+        // Verify decoding without automaticNattKeepaliveTimerEnabled defaults to false
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
+        assertFalse(decoded.automaticNattKeepaliveTimerEnabled);
+    }
+
+    @Test
+    public void testEncodeDecodeMissingAutomaticIpVersionSelectionEnabled() {
+        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
+
+        // Verify decoding without automaticIpVersionSelectionEnabled defaults to false
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
+        assertFalse(decoded.automaticIpVersionSelectionEnabled);
+    }
+
+    @Test
     public void testEncodeDecodeLoginsNotSaved() {
         final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
         profile.saveLogin = false;
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index 0e17cd7..d189848 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -690,6 +690,80 @@
                 mBpfNetMaps.setUidRule(FIREWALL_CHAIN_DOZABLE, TEST_UID, FIREWALL_RULE_ALLOW));
     }
 
+    private void doTestGetUidRule(final List<Integer> enableChains) throws Exception {
+        mUidOwnerMap.updateEntry(new S32(TEST_UID), new UidOwnerValue(0, getMatch(enableChains)));
+
+        for (final int chain: FIREWALL_CHAINS) {
+            final String testCase = "EnabledChains: " + enableChains + " CheckedChain: " + chain;
+            if (enableChains.contains(chain)) {
+                final int expectedRule = mBpfNetMaps.isFirewallAllowList(chain)
+                        ? FIREWALL_RULE_ALLOW : FIREWALL_RULE_DENY;
+                assertEquals(testCase, expectedRule, mBpfNetMaps.getUidRule(chain, TEST_UID));
+            } else {
+                final int expectedRule = mBpfNetMaps.isFirewallAllowList(chain)
+                        ? FIREWALL_RULE_DENY : FIREWALL_RULE_ALLOW;
+                assertEquals(testCase, expectedRule, mBpfNetMaps.getUidRule(chain, TEST_UID));
+            }
+        }
+    }
+
+    private void doTestGetUidRule(final int enableChain) throws Exception {
+        doTestGetUidRule(List.of(enableChain));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testGetUidRule() throws Exception {
+        doTestGetUidRule(FIREWALL_CHAIN_DOZABLE);
+        doTestGetUidRule(FIREWALL_CHAIN_STANDBY);
+        doTestGetUidRule(FIREWALL_CHAIN_POWERSAVE);
+        doTestGetUidRule(FIREWALL_CHAIN_RESTRICTED);
+        doTestGetUidRule(FIREWALL_CHAIN_LOW_POWER_STANDBY);
+        doTestGetUidRule(FIREWALL_CHAIN_OEM_DENY_1);
+        doTestGetUidRule(FIREWALL_CHAIN_OEM_DENY_2);
+        doTestGetUidRule(FIREWALL_CHAIN_OEM_DENY_3);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testGetUidRuleMultipleChainEnabled() throws Exception {
+        doTestGetUidRule(List.of(
+                FIREWALL_CHAIN_DOZABLE,
+                FIREWALL_CHAIN_STANDBY));
+        doTestGetUidRule(List.of(
+                FIREWALL_CHAIN_DOZABLE,
+                FIREWALL_CHAIN_STANDBY,
+                FIREWALL_CHAIN_POWERSAVE,
+                FIREWALL_CHAIN_RESTRICTED));
+        doTestGetUidRule(FIREWALL_CHAINS);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testGetUidRuleNoEntry() throws Exception {
+        mUidOwnerMap.clear();
+        for (final int chain: FIREWALL_CHAINS) {
+            final int expectedRule = mBpfNetMaps.isFirewallAllowList(chain)
+                    ? FIREWALL_RULE_DENY : FIREWALL_RULE_ALLOW;
+            assertEquals(expectedRule, mBpfNetMaps.getUidRule(chain, TEST_UID));
+        }
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testGetUidRuleInvalidChain() {
+        final Class<ServiceSpecificException> expected = ServiceSpecificException.class;
+        assertThrows(expected, () -> mBpfNetMaps.getUidRule(-1 /* childChain */, TEST_UID));
+        assertThrows(expected, () -> mBpfNetMaps.getUidRule(1000 /* childChain */, TEST_UID));
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.S_V2)
+    public void testGetUidRuleBeforeT() {
+        assertThrows(UnsupportedOperationException.class,
+                () -> mBpfNetMaps.getUidRule(FIREWALL_CHAIN_DOZABLE, TEST_UID));
+    }
+
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     public void testReplaceUidChain() throws Exception {
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 634d5c9..ea50999 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -27,6 +27,7 @@
 import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
 import static android.Manifest.permission.NETWORK_FACTORY;
 import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.Manifest.permission.NETWORK_SETUP_WIZARD;
 import static android.Manifest.permission.NETWORK_STACK;
 import static android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD;
 import static android.app.PendingIntent.FLAG_IMMUTABLE;
@@ -5320,6 +5321,13 @@
         callback.expectAvailableCallbacksUnvalidated(mCellAgent);
         mCm.unregisterNetworkCallback(callback);
 
+        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_DENIED);
+        mServiceContext.setPermission(NETWORK_SETUP_WIZARD, PERMISSION_GRANTED);
+        mCm.registerSystemDefaultNetworkCallback(callback, handler);
+        callback.expectAvailableCallbacksUnvalidated(mCellAgent);
+        mCm.unregisterNetworkCallback(callback);
+
+        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
         mCm.registerDefaultNetworkCallbackForUid(APP1_UID, callback, handler);
         callback.expectAvailableCallbacksUnvalidated(mCellAgent);
         mCm.unregisterNetworkCallback(callback);