Merge "Add CtsNetTestCasesMaxTargetSdk30"
diff --git a/staticlibs/device/com/android/net/module/util/structs/Icmpv4Header.java b/staticlibs/device/com/android/net/module/util/structs/Icmpv4Header.java
new file mode 100644
index 0000000..454bb07
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/Icmpv4Header.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.net.module.util.structs;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/**
+ * ICMPv4 header as per https://tools.ietf.org/html/rfc792.
+ *
+ * 0                   1                   2                   3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |     Type      |     Code      |          Checksum             |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class Icmpv4Header extends Struct {
+    @Field(order = 0, type = Type.U8)
+    public short type;
+    @Field(order = 1, type = Type.U8)
+    public short code;
+    @Field(order = 2, type = Type.S16)
+    public short checksum;
+    public Icmpv4Header(final short type, final short code, final short checksum) {
+        this.type = type;
+        this.code = code;
+        this.checksum = checksum;
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index 353fe69..1ad9c35 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -228,6 +228,35 @@
     public static final int TAG_SYSTEM_PROBE = 0xFFFFFF81;
     public static final int TAG_SYSTEM_DNS = 0xFFFFFF82;
 
+    /**
+     * A test URL used to override configuration settings and overlays for the network validation
+     * HTTPS URL, when set in {@link android.provider.DeviceConfig} configuration.
+     *
+     * <p>This URL will be ignored if the host is not "localhost" (it can only be used to test with
+     * a local test server), and must not be set in production scenarios (as enforced by CTS tests).
+     *
+     * <p>{@link #TEST_URL_EXPIRATION_TIME} must also be set to use this setting.
+     */
+    public static final String TEST_CAPTIVE_PORTAL_HTTPS_URL = "test_captive_portal_https_url";
+    /**
+     * A test URL used to override configuration settings and overlays for the network validation
+     * HTTP URL, when set in {@link android.provider.DeviceConfig} configuration.
+     *
+     * <p>This URL will be ignored if the host is not "localhost" (it can only be used to test with
+     * a local test server), and must not be set in production scenarios (as enforced by CTS tests).
+     *
+     * <p>{@link #TEST_URL_EXPIRATION_TIME} must also be set to use this setting.
+     */
+    public static final String TEST_CAPTIVE_PORTAL_HTTP_URL = "test_captive_portal_http_url";
+    /**
+     * Expiration time of the test URL, in ms, relative to {@link System#currentTimeMillis()}.
+     *
+     * <p>After this expiration time, test URLs will be ignored. They will also be ignored if
+     * the expiration time is more than 10 minutes in the future, to avoid misconfiguration
+     * following test runs.
+     */
+    public static final String TEST_URL_EXPIRATION_TIME = "test_url_expiration_time";
+
     // TODO: Move to Inet4AddressUtils
     // See aosp/1455936: NetworkStackConstants can't depend on it as it causes jarjar-related issues
     // for users of both the net-utils-device-common and net-utils-framework-common libraries.
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
index 236318d..35d9f31 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
@@ -46,9 +46,10 @@
  *
  * While this will work outside of mainline too, there just is no point to
  * using it when the .o and the bpfloader ship in sync with each other.
+ * In which case it's just best to use the default.
  */
 #ifndef BPFLOADER_MIN_VER
-#define BPFLOADER_MIN_VER DEFAULT_BPFLOADER_MIN_VER
+#define BPFLOADER_MIN_VER COMPILE_FOR_BPFLOADER_VERSION
 #endif
 
 #ifndef BPFLOADER_MAX_VER
@@ -192,8 +193,8 @@
 
 #ifndef DEFAULT_BPF_MAP_UID
 #define DEFAULT_BPF_MAP_UID AID_ROOT
-#elif BPFLOADER_MIN_VER < 21u
-#error "Bpf Map UID must be left at default of AID_ROOT for BpfLoader prior to v0.21"
+#elif BPFLOADER_MIN_VER < 28u
+#error "Bpf Map UID must be left at default of AID_ROOT for BpfLoader prior to v0.28"
 #endif
 
 #define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md) \
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
index 14a0295..02caf07 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
@@ -48,6 +48,10 @@
 #define DEFAULT_SIZEOF_BPF_MAP_DEF 32       // v0.0 struct: enum (uint sized) + 7 uint
 #define DEFAULT_SIZEOF_BPF_PROG_DEF 20      // v0.0 struct: 4 uint + bool + 3 byte alignment pad
 
+// By default, unless otherwise specified, allow the use of features only supported by v0.28,
+// which first added working support for map uid != root
+#define COMPILE_FOR_BPFLOADER_VERSION 28u
+
 /*
  * The bpf_{map,prog}_def structures are compiled for different architectures.
  * Once by the BPF compiler for the BPF architecture, and once by a C++
@@ -143,7 +147,7 @@
     //   unsigned int inner_map_idx;
     //   unsigned int numa_node;
 
-    unsigned int uid;   // uid_t
+    unsigned int zero;  // uid_t, for compat with old (buggy) bpfloader must be AID_ROOT == 0
     unsigned int gid;   // gid_t
     unsigned int mode;  // mode_t
 
@@ -171,13 +175,15 @@
 
     bool shared;  // use empty string as 'file' component of pin path - allows cross .o map sharing
     char pad0[3];  // manually pad up to 4 byte alignment, may be used for extensions in the future
+
+    unsigned int uid;   // uid_t
 };
 
 _Static_assert(sizeof(((struct bpf_map_def *)0)->selinux_context) == 32, "must be 32 bytes");
 _Static_assert(sizeof(((struct bpf_map_def *)0)->pin_subdir) == 32, "must be 32 bytes");
 
 // This needs to be updated whenever the above structure definition is expanded.
-_Static_assert(sizeof(struct bpf_map_def) == 116, "sizeof struct bpf_map_def != 116");
+_Static_assert(sizeof(struct bpf_map_def) == 120, "sizeof struct bpf_map_def != 120");
 _Static_assert(__alignof__(struct bpf_map_def) == 4, "__alignof__ struct bpf_map_def != 4");
 _Static_assert(_Alignof(struct bpf_map_def) == 4, "_Alignof struct bpf_map_def != 4");
 
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/SharedLogTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/SharedLogTest.java
index 446e881..aa1bfee 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/SharedLogTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/SharedLogTest.java
@@ -17,6 +17,8 @@
 package com.android.net.module.util;
 
 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 androidx.test.filters.SmallTest;
@@ -27,16 +29,24 @@
 
 import java.io.ByteArrayOutputStream;
 import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class SharedLogTest {
     private static final String TIMESTAMP_PATTERN = "\\d{2}:\\d{2}:\\d{2}";
     private static final String TIMESTAMP = "HH:MM:SS";
+    private static final String TAG = "top";
 
     @Test
     public void testBasicOperation() {
-        final SharedLog logTop = new SharedLog("top");
+        final SharedLog logTop = new SharedLog(TAG);
+        assertTrue(TAG.equals(logTop.getTag()));
+
         logTop.mark("first post!");
 
         final SharedLog logLevel2a = logTop.forSubComponent("twoA");
@@ -48,8 +58,10 @@
 
         final SharedLog logLevel3 = logLevel2a.forSubComponent("three");
         logTop.log("still logging");
-        logLevel3.log("3 >> 2");
+        logLevel2b.e(new Exception("Got another exception"));
+        logLevel3.i("3 >> 2");
         logLevel2a.mark("ok: last post");
+        logTop.logf("finished!");
 
         final String[] expected = {
             " - MARK first post!",
@@ -59,29 +71,53 @@
             " - [twoB] ERROR Wait, here's one: Test",
             " - [twoA] WARN second post?",
             " - still logging",
+            " - [twoB] ERROR java.lang.Exception: Got another exception",
             " - [twoA.three] 3 >> 2",
             " - [twoA] MARK ok: last post",
+            " - finished!",
         };
         // Verify the logs are all there and in the correct order.
-        verifyLogLines(expected, logTop);
+        assertDumpLogs(expected, logTop);
 
         // In fact, because they all share the same underlying LocalLog,
         // every subcomponent SharedLog's dump() is identical.
-        verifyLogLines(expected, logLevel2a);
-        verifyLogLines(expected, logLevel2b);
-        verifyLogLines(expected, logLevel3);
+        assertDumpLogs(expected, logLevel2a);
+        assertDumpLogs(expected, logLevel2b);
+        assertDumpLogs(expected, logLevel3);
     }
 
-    private static void verifyLogLines(String[] expected, SharedLog log) {
+    private static void assertDumpLogs(String[] expected, SharedLog log) {
+        verifyLogLines(expected, dump(log));
+        verifyLogLines(reverse(expected), reverseDump(log));
+    }
+
+    private static String dump(SharedLog log) {
+        return getSharedLogString(pw -> log.dump(null /* fd */, pw, null /* args */));
+    }
+
+    private static String reverseDump(SharedLog log) {
+        return getSharedLogString(pw -> log.reverseDump(pw));
+    }
+
+    private static String[] reverse(String[] ary) {
+        final List<String> ls = new ArrayList<>(Arrays.asList(ary));
+        Collections.reverse(ls);
+        return ls.toArray(new String[ary.length]);
+    }
+
+    private static String getSharedLogString(Consumer<PrintWriter> functor) {
         final ByteArrayOutputStream ostream = new ByteArrayOutputStream();
         final PrintWriter pw = new PrintWriter(ostream, true);
-        log.dump(null, pw, null);
+        functor.accept(pw);
 
         final String dumpOutput = ostream.toString();
-        assertTrue(dumpOutput != null);
-        assertTrue(!"".equals(dumpOutput));
+        assertNotNull(dumpOutput);
+        assertFalse("".equals(dumpOutput));
+        return dumpOutput;
+    }
 
-        final String[] lines = dumpOutput.split("\n");
+    private static void verifyLogLines(String[] expected, String gottenLogs) {
+        final String[] lines = gottenLogs.split("\n");
         assertEquals(expected.length, lines.length);
 
         for (int i = 0; i < expected.length; i++) {
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/CompatUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/CompatUtil.kt
index ff8c668..82f1d9b 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/CompatUtil.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/CompatUtil.kt
@@ -17,7 +17,7 @@
 package com.android.testutils
 
 import android.net.NetworkSpecifier
-import android.os.Build
+import com.android.modules.utils.build.SdkLevel.isAtLeastS
 
 /**
  * Test utility to create [NetworkSpecifier]s on different SDK versions.
@@ -26,7 +26,7 @@
     @JvmStatic
     fun makeTestNetworkSpecifier(ifName: String): NetworkSpecifier {
         // Until R, there was no TestNetworkSpecifier, StringNetworkSpecifier was used instead
-        if (isDevSdkInRange(minExclusive = null, maxInclusive = Build.VERSION_CODES.R)) {
+        if (!isAtLeastS()) {
             return makeNetworkSpecifierInternal("android.net.StringNetworkSpecifier", ifName)
         }
         // TestNetworkSpecifier is not part of the SDK in some branches using this utility
@@ -37,7 +37,7 @@
     @JvmStatic
     fun makeEthernetNetworkSpecifier(ifName: String): NetworkSpecifier {
         // Until R, there was no EthernetNetworkSpecifier, StringNetworkSpecifier was used instead
-        if (isDevSdkInRange(minExclusive = null, maxInclusive = Build.VERSION_CODES.R)) {
+        if (!isAtLeastS()) {
             return makeNetworkSpecifierInternal("android.net.StringNetworkSpecifier", ifName)
         }
         // EthernetNetworkSpecifier is not part of the SDK in some branches using this utility
@@ -50,4 +50,4 @@
         return Class.forName(clazz)
                 .getConstructor(String::class.java).newInstance(specifier) as NetworkSpecifier
     }
-}
\ No newline at end of file
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
index 3fcf801..21e84da 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
@@ -18,58 +18,41 @@
 
 import android.os.Build
 import androidx.test.InstrumentationRegistry
-import com.android.modules.utils.build.SdkLevel
-import kotlin.test.fail
+import com.android.modules.utils.build.UnboundedSdkLevel
 import org.junit.Assume.assumeTrue
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
 import java.util.regex.Pattern
 
-// TODO: Remove it when Build.VERSION_CODES.SC_V2 is available
-const val SC_V2 = 32
+@Deprecated("Use Build.VERSION_CODES", ReplaceWith("Build.VERSION_CODES.S_V2"))
+const val SC_V2 = Build.VERSION_CODES.S_V2
 
 private val MAX_TARGET_SDK_ANNOTATION_RE = Pattern.compile("MaxTargetSdk([0-9]+)$")
 private val targetSdk = InstrumentationRegistry.getContext().applicationInfo.targetSdkVersion
 
+private fun isDevSdkInRange(minExclusive: String?, maxInclusive: String?): Boolean {
+    return (minExclusive == null || !UnboundedSdkLevel.isAtMost(minExclusive)) &&
+            (maxInclusive == null || UnboundedSdkLevel.isAtMost(maxInclusive))
+}
+
 /**
- * Returns true if the development SDK version of the device is in the provided range.
+ * Returns true if the development SDK version of the device is in the provided annotation range.
  *
- * If the device is not using a release SDK, the development SDK is considered to be higher than
- * [Build.VERSION.SDK_INT].
+ * If the device is not using a release SDK, the development SDK differs from
+ * [Build.VERSION.SDK_INT], and is indicated by the device codenames; see [UnboundedSdkLevel].
  */
-fun isDevSdkInRange(minExclusive: Int?, maxInclusive: Int?): Boolean {
-    return (minExclusive == null || isDevSdkAfter(minExclusive)) &&
-            (maxInclusive == null || isDevSdkUpTo(maxInclusive))
-}
-
-private fun isDevSdkAfter(minExclusive: Int): Boolean {
-    // A development build for T typically has SDK_INT = 30 (R) or SDK_INT = 31 (S), so SDK_INT
-    // alone cannot be used to check the SDK version.
-    // For recent SDKs that still have development builds used for testing, use SdkLevel utilities
-    // instead of SDK_INT.
-    return when (minExclusive) {
-        // TODO: Use Build.VERSION_CODES.SC_V2 when it is available
-        SC_V2 -> SdkLevel.isAtLeastT()
-        // TODO: To use SdkLevel.isAtLeastSv2 when available
-        Build.VERSION_CODES.S -> fail("Do you expect to ignore the test until T? Use SC_V2 instead")
-        Build.VERSION_CODES.R -> SdkLevel.isAtLeastS()
-        // Development builds of SDK versions <= R are not used anymore
-        else -> Build.VERSION.SDK_INT > minExclusive
-    }
-}
-
-private fun isDevSdkUpTo(maxInclusive: Int): Boolean {
-    return when (maxInclusive) {
-        // TODO: Use Build.VERSION_CODES.SC_V2 when it is available
-        SC_V2 -> !SdkLevel.isAtLeastT()
-        // TODO: To use SdkLevel.isAtLeastSv2 when available
-        Build.VERSION_CODES.S ->
-                fail("Do you expect to ignore the test before T? Use SC_V2 instead")
-        Build.VERSION_CODES.R -> !SdkLevel.isAtLeastS()
-        // Development builds of SDK versions <= R are not used anymore
-        else -> Build.VERSION.SDK_INT <= maxInclusive
-    }
+fun isDevSdkInRange(
+    ignoreUpTo: DevSdkIgnoreRule.IgnoreUpTo?,
+    ignoreAfter: DevSdkIgnoreRule.IgnoreAfter?
+): Boolean {
+    val minExclusive =
+            if (ignoreUpTo?.value == 0) ignoreUpTo.codename
+            else ignoreUpTo?.value?.toString()
+    val maxInclusive =
+            if (ignoreAfter?.value == 0) ignoreAfter.codename
+            else ignoreAfter?.value?.toString()
+    return isDevSdkInRange(minExclusive, maxInclusive)
 }
 
 private fun getMaxTargetSdk(description: Description): Int? {
@@ -86,13 +69,23 @@
  * If the device is not using a release SDK, the development SDK is considered to be higher than
  * [Build.VERSION.SDK_INT].
  *
- * @param ignoreClassUpTo Skip all tests in the class if the device dev SDK is <= this value.
- * @param ignoreClassAfter Skip all tests in the class if the device dev SDK is > this value.
+ * @param ignoreClassUpTo Skip all tests in the class if the device dev SDK is <= this codename or
+ *                        SDK level.
+ * @param ignoreClassAfter Skip all tests in the class if the device dev SDK is > this codename or
+ *                         SDK level.
  */
 class DevSdkIgnoreRule @JvmOverloads constructor(
-    private val ignoreClassUpTo: Int? = null,
-    private val ignoreClassAfter: Int? = null
+    private val ignoreClassUpTo: String? = null,
+    private val ignoreClassAfter: String? = null
 ) : TestRule {
+    /**
+     * @param ignoreClassUpTo Skip all tests in the class if the device dev SDK is <= this value.
+     * @param ignoreClassAfter Skip all tests in the class if the device dev SDK is > this value.
+     */
+    @JvmOverloads
+    constructor(ignoreClassUpTo: Int?, ignoreClassAfter: Int? = null) : this(
+            ignoreClassUpTo?.toString(), ignoreClassAfter?.toString())
+
     override fun apply(base: Statement, description: Description): Statement {
         return IgnoreBySdkStatement(base, description)
     }
@@ -103,7 +96,7 @@
      * If the device is not using a release SDK, the development SDK is considered to be higher
      * than [Build.VERSION.SDK_INT].
      */
-    annotation class IgnoreAfter(val value: Int)
+    annotation class IgnoreAfter(val value: Int = 0, val codename: String = "")
 
     /**
      * Ignore the test for any development SDK that lower than or equal to [value].
@@ -111,7 +104,7 @@
      * If the device is not using a release SDK, the development SDK is considered to be higher
      * than [Build.VERSION.SDK_INT].
      */
-    annotation class IgnoreUpTo(val value: Int)
+    annotation class IgnoreUpTo(val value: Int = 0, val codename: String = "")
 
     private inner class IgnoreBySdkStatement(
         private val base: Statement,
@@ -124,7 +117,7 @@
             val devSdkMessage = "Skipping test for build ${Build.VERSION.CODENAME} " +
                     "with SDK ${Build.VERSION.SDK_INT}"
             assumeTrue(devSdkMessage, isDevSdkInRange(ignoreClassUpTo, ignoreClassAfter))
-            assumeTrue(devSdkMessage, isDevSdkInRange(ignoreUpTo?.value, ignoreAfter?.value))
+            assumeTrue(devSdkMessage, isDevSdkInRange(ignoreUpTo, ignoreAfter))
 
             val maxTargetSdk = getMaxTargetSdk(description)
             if (maxTargetSdk != null) {
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
index da7bf97..2e73666 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
@@ -52,7 +52,7 @@
         val ignoreAfter = it.getAnnotation(IgnoreAfter::class.java)
         val ignoreUpTo = it.getAnnotation(IgnoreUpTo::class.java)
 
-        if (isDevSdkInRange(ignoreUpTo?.value, ignoreAfter?.value)) AndroidJUnit4(klass) else null
+        if (isDevSdkInRange(ignoreUpTo, ignoreAfter)) AndroidJUnit4(klass) else null
     }
 
     override fun run(notifier: RunNotifier) {
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketResponder.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketResponder.kt
index daf29e4..964c6c6 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/PacketResponder.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketResponder.kt
@@ -54,6 +54,7 @@
      */
     fun stop() {
         replyThread.interrupt()
+        replyThread.join()
     }
 
     private inner class ReplyThread(name: String) : Thread(name) {
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/RouterAdvertisementResponder.java b/staticlibs/testutils/devicetests/com/android/testutils/RouterAdvertisementResponder.java
index 4ad93d8..478161d 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/RouterAdvertisementResponder.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/RouterAdvertisementResponder.java
@@ -18,12 +18,15 @@
 
 import static android.system.OsConstants.IPPROTO_ICMPV6;
 
-import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
-import static com.android.net.module.util.NetworkStackConstants.ETHER_SRC_ADDR_OFFSET;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_SOLICITATION;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST;
-import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
+import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
+import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER;
+import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
 import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_AUTONOMOUS;
 import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_ON_LINK;
 
@@ -31,6 +34,8 @@
 import android.net.IpPrefix;
 import android.net.LinkAddress;
 import android.net.MacAddress;
+import android.util.ArrayMap;
+import android.util.Log;
 import android.util.Pair;
 
 import com.android.net.module.util.Ipv6Utils;
@@ -38,18 +43,19 @@
 import com.android.net.module.util.structs.EthernetHeader;
 import com.android.net.module.util.structs.Icmpv6Header;
 import com.android.net.module.util.structs.Ipv6Header;
+import com.android.net.module.util.structs.LlaOption;
+import com.android.net.module.util.structs.NsHeader;
 import com.android.net.module.util.structs.PrefixInformationOption;
 import com.android.net.module.util.structs.RdnssOption;
 
 import java.io.IOException;
 import java.net.Inet6Address;
 import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
+import java.util.Map;
 
 /**
- * RA responder class useful for tests that require a provisioned interface.
+ * ND (RA & NA) responder class useful for tests that require a provisioned IPv6 interface.
+ * TODO: rename to NdResponder
  */
 public class RouterAdvertisementResponder extends PacketResponder {
     private static final String TAG = "RouterAdvertisementResponder";
@@ -57,26 +63,28 @@
     private static final Inet6Address DNS_SERVER =
             (Inet6Address) InetAddresses.parseNumericAddress("2001:4860:4860::64");
     private final TapPacketReader mPacketReader;
-    private final List<Pair<MacAddress, Inet6Address>> mRouterList = new ArrayList<>();
+    // Maps IPv6 address to MacAddress and isRouter boolean.
+    private final Map<Inet6Address, Pair<MacAddress, Boolean>> mNeighborMap = new ArrayMap<>();
 
     public RouterAdvertisementResponder(TapPacketReader packetReader) {
-        super(packetReader, RouterAdvertisementResponder::isRouterSolicitation, TAG);
+        super(packetReader, RouterAdvertisementResponder::isRsOrNs, TAG);
         mPacketReader = packetReader;
     }
 
-    private static boolean isRouterSolicitation(byte[] packet) {
+    /** Returns true if the packet is a router solicitation or neighbor solicitation message. */
+    private static boolean isRsOrNs(byte[] packet) {
         final ByteBuffer buffer = ByteBuffer.wrap(packet);
         final EthernetHeader ethHeader = Struct.parse(EthernetHeader.class, buffer);
         if (ethHeader.etherType != ETHER_TYPE_IPV6) {
             return false;
         }
         final Ipv6Header ipv6Header = Struct.parse(Ipv6Header.class, buffer);
-        if (ipv6Header.nextHeader != IPPROTO_ICMPV6
-                || !ipv6Header.dstIp.equals(IPV6_ADDR_ALL_ROUTERS_MULTICAST)) {
+        if (ipv6Header.nextHeader != IPPROTO_ICMPV6) {
             return false;
         }
         final Icmpv6Header icmpv6Header = Struct.parse(Icmpv6Header.class, buffer);
-        return icmpv6Header.type == ICMPV6_ROUTER_SOLICITATION;
+        return icmpv6Header.type == ICMPV6_ROUTER_SOLICITATION
+            || icmpv6Header.type == ICMPV6_NEIGHBOR_SOLICITATION;
     }
 
     /**
@@ -85,7 +93,16 @@
      * @param ip the link-local address of the router.
      */
     public void addRouterEntry(MacAddress mac, Inet6Address ip) {
-        mRouterList.add(new Pair<>(mac, ip));
+        mNeighborMap.put(ip, new Pair<>(mac, true));
+    }
+
+    /**
+     * Adds a new neighbor to be advertised.
+     * @param mac the mac address of the neighbor.
+     * @param ip the link-local address of the neighbor.
+     */
+    public void addNeighborEntry(MacAddress mac, Inet6Address ip) {
+        mNeighborMap.put(ip, new Pair<>(mac, false));
     }
 
     private ByteBuffer buildPrefixOption() {
@@ -99,25 +116,72 @@
         return RdnssOption.build(3600/*lifetime, must be at least 120*/, DNS_SERVER);
     }
 
+    private ByteBuffer buildSllaOption(MacAddress srcMac) {
+        return LlaOption.build((byte) ICMPV6_ND_OPTION_SLLA, srcMac);
+    }
+
     private ByteBuffer buildRaPacket(MacAddress srcMac, MacAddress dstMac, Inet6Address srcIp) {
         return Ipv6Utils.buildRaPacket(srcMac, dstMac, srcIp, IPV6_ADDR_ALL_NODES_MULTICAST,
                 (byte) 0 /*M=0, O=0*/, 3600 /*lifetime*/, 0 /*reachableTime, unspecified*/,
-                0/*retransTimer, unspecified*/, buildPrefixOption(), buildRdnssOption());
+                0/*retransTimer, unspecified*/, buildPrefixOption(), buildRdnssOption(),
+                buildSllaOption(srcMac));
+    }
+
+    private static void sendResponse(TapPacketReader reader, ByteBuffer buffer) {
+        try {
+            reader.sendResponse(buffer);
+        } catch (IOException e) {
+            // Throwing an exception here will crash the test process. Let's stick to logging, as
+            // the test will fail either way.
+            Log.e(TAG, "Failed to send buffer", e);
+        }
+    }
+
+    private void replyToRouterSolicitation(TapPacketReader reader, MacAddress dstMac) {
+        for (Map.Entry<Inet6Address, Pair<MacAddress, Boolean>> it : mNeighborMap.entrySet()) {
+            final boolean isRouter = it.getValue().second;
+            if (!isRouter) {
+                continue;
+            }
+            final ByteBuffer raResponse = buildRaPacket(it.getValue().first, dstMac, it.getKey());
+            sendResponse(reader, raResponse);
+        }
+    }
+
+    private void replyToNeighborSolicitation(TapPacketReader reader, MacAddress dstMac,
+            Inet6Address dstIp, Inet6Address targetIp) {
+        final Pair<MacAddress, Boolean> neighbor = mNeighborMap.get(targetIp);
+        if (neighbor == null) {
+            return;
+        }
+
+        final MacAddress srcMac = neighbor.first;
+        final boolean isRouter = neighbor.second;
+        int flags = NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED | NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
+        if (isRouter) {
+            flags |= NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER;
+        }
+
+        final ByteBuffer tlla = LlaOption.build((byte) ICMPV6_ND_OPTION_TLLA, srcMac);
+        final ByteBuffer naResponse = Ipv6Utils.buildNaPacket(srcMac, dstMac, targetIp, dstIp,
+                flags, targetIp, tlla);
+        sendResponse(reader, naResponse);
     }
 
     @Override
     protected void replyToPacket(byte[] packet, TapPacketReader reader) {
-        final MacAddress srcMac = MacAddress.fromBytes(
-                Arrays.copyOfRange(packet, ETHER_SRC_ADDR_OFFSET,
-                        ETHER_SRC_ADDR_OFFSET + ETHER_ADDR_LEN));
+        final ByteBuffer buf = ByteBuffer.wrap(packet);
+        // Messages are filtered by parent class, so it is safe to assume that packet is either an
+        // RS or NS.
+        final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf);
+        final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf);
+        final Icmpv6Header icmpv6Header = Struct.parse(Icmpv6Header.class, buf);
 
-        for (Pair<MacAddress, Inet6Address> it : mRouterList) {
-            final ByteBuffer raResponse = buildRaPacket(it.first, srcMac, it.second);
-            try {
-                reader.sendResponse(raResponse);
-            } catch (IOException e) {
-                throw new RuntimeException("Failed to send RA");
-            }
+        if (icmpv6Header.type == ICMPV6_ROUTER_SOLICITATION) {
+            replyToRouterSolicitation(reader, ethHdr.srcMac);
+        } else if (icmpv6Header.type == ICMPV6_NEIGHBOR_SOLICITATION) {
+            final NsHeader nsHeader = Struct.parse(NsHeader.class, buf);
+            replyToNeighborSolicitation(reader, ethHdr.srcMac, ipv6Hdr.srcIp, nsHeader.target);
         }
     }
 }
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestPermissionUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestPermissionUtil.kt
index f557f18..a4dbd9a 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestPermissionUtil.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestPermissionUtil.kt
@@ -19,6 +19,7 @@
 package com.android.testutils
 
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel
 import com.android.testutils.ExceptionUtils.ThrowingRunnable
 import com.android.testutils.ExceptionUtils.ThrowingSupplier
 
@@ -31,6 +32,23 @@
  */
 fun <T> runAsShell(vararg permissions: String, task: () -> T): T {
     val autom = InstrumentationRegistry.getInstrumentation().uiAutomation
+
+    // Calls to adoptShellPermissionIdentity do not nest, and dropShellPermissionIdentity drops all
+    // permissions. Thus, nesting calls will almost certainly cause test bugs, On S+, where we can
+    // detect this, refuse to do it.
+    //
+    // TODO: when R is deprecated, we could try to make this work instead.
+    // - Get the list of previously-adopted permissions.
+    // - Adopt the union of the previously-adopted and newly-requested permissions.
+    // - Run the task.
+    // - Adopt the previously-adopted permissions, dropping the ones just adopted.
+    //
+    // This would allow tests (and utility classes, such as the TestCarrierConfigReceiver attempted
+    // in aosp/2106007) to call runAsShell even within a test that has already adopted permissions.
+    if (SdkLevel.isAtLeastS() && !autom.getAdoptedShellPermissions().isNullOrEmpty()) {
+        throw IllegalStateException("adoptShellPermissionIdentity calls must not be nested")
+    }
+
     autom.adoptShellPermissionIdentity(*permissions)
     try {
         return task()