Merge "[Thread] add @RequiresPermissions for unregisterXxxCallback APIs" into main
diff --git a/framework/src/android/net/QosSession.java b/framework/src/android/net/QosSession.java
index 25f3965..d1edae9 100644
--- a/framework/src/android/net/QosSession.java
+++ b/framework/src/android/net/QosSession.java
@@ -22,6 +22,9 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * Provides identifying information of a QoS session.  Sent to an application through
  * {@link QosCallback}.
@@ -107,6 +110,7 @@
             TYPE_EPS_BEARER,
             TYPE_NR_BEARER,
     })
+    @Retention(RetentionPolicy.SOURCE)
     @interface QosSessionType {}
 
     private QosSession(final Parcel in) {
diff --git a/nearby/framework/java/android/nearby/BroadcastRequest.java b/nearby/framework/java/android/nearby/BroadcastRequest.java
index 90f4d0f..6d6357d 100644
--- a/nearby/framework/java/android/nearby/BroadcastRequest.java
+++ b/nearby/framework/java/android/nearby/BroadcastRequest.java
@@ -88,6 +88,7 @@
      * @hide
      */
     @IntDef({MEDIUM_BLE})
+    @Retention(RetentionPolicy.SOURCE)
     public @interface Medium {}
 
     /**
diff --git a/nearby/framework/java/android/nearby/NearbyDevice.java b/nearby/framework/java/android/nearby/NearbyDevice.java
index e8fcc28..e7db0c5 100644
--- a/nearby/framework/java/android/nearby/NearbyDevice.java
+++ b/nearby/framework/java/android/nearby/NearbyDevice.java
@@ -25,6 +25,8 @@
 
 import com.android.internal.util.Preconditions;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
@@ -149,6 +151,7 @@
      * @hide
      */
     @IntDef({Medium.BLE, Medium.BLUETOOTH})
+    @Retention(RetentionPolicy.SOURCE)
     public @interface Medium {
         int BLE = 1;
         int BLUETOOTH = 2;
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index a70b303..070a2b6 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -34,6 +34,8 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.Preconditions;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
 import java.util.Objects;
 import java.util.WeakHashMap;
@@ -63,6 +65,7 @@
             ScanStatus.SUCCESS,
             ScanStatus.ERROR,
     })
+    @Retention(RetentionPolicy.SOURCE)
     public @interface ScanStatus {
         // The undetermined status, some modules may be initializing. Retry is suggested.
         int UNKNOWN = 0;
diff --git a/staticlibs/framework/com/android/net/module/util/DnsSvcbPacket.java b/staticlibs/framework/com/android/net/module/util/DnsSvcbPacket.java
index c7ed3e6..d298599 100644
--- a/staticlibs/framework/com/android/net/module/util/DnsSvcbPacket.java
+++ b/staticlibs/framework/com/android/net/module/util/DnsSvcbPacket.java
@@ -21,7 +21,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.text.TextUtils;
 import android.util.Log;
 
 import java.net.InetAddress;
@@ -29,7 +28,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.StringJoiner;
 
 /**
  * A class for a DNS SVCB response packet.
@@ -159,16 +157,6 @@
         return out;
     }
 
-    @Override
-    public String toString() {
-        final StringJoiner out = new StringJoiner(" ");
-        out.add("QUERY: [" + TextUtils.join(", ", mRecords[QDSECTION]) + "]");
-        out.add("ANSWER: [" + TextUtils.join(", ", mRecords[ANSECTION]) + "]");
-        out.add("AUTHORITY: [" + TextUtils.join(", ", mRecords[NSSECTION]) + "]");
-        out.add("ADDITIONAL: [" + TextUtils.join(", ", mRecords[ARSECTION]) + "]");
-        return out.toString();
-    }
-
     /**
      * Creates a DnsSvcbPacket object from the given wire-format DNS answer.
      */
diff --git a/staticlibs/framework/com/android/net/module/util/DnsSvcbRecord.java b/staticlibs/framework/com/android/net/module/util/DnsSvcbRecord.java
index 669725c..935cdf6 100644
--- a/staticlibs/framework/com/android/net/module/util/DnsSvcbRecord.java
+++ b/staticlibs/framework/com/android/net/module/util/DnsSvcbRecord.java
@@ -230,7 +230,7 @@
     /**
      * The base class for all SvcParam.
      */
-    private abstract static class SvcParam {
+    private abstract static class SvcParam<T> {
         private final int mKey;
 
         SvcParam(int key) {
@@ -240,9 +240,11 @@
         int getKey() {
             return mKey;
         }
+
+        abstract T getValue();
     }
 
-    private static class SvcParamMandatory extends SvcParam {
+    private static class SvcParamMandatory extends SvcParam<short[]> {
         private final short[] mValue;
 
         private SvcParamMandatory(@NonNull ByteBuffer buf) throws BufferUnderflowException,
@@ -258,6 +260,12 @@
         }
 
         @Override
+        short[] getValue() {
+            /* Not yet implemented */
+            return null;
+        }
+
+        @Override
         public String toString() {
             final StringJoiner valueJoiner = new StringJoiner(",");
             for (short key : mValue) {
@@ -267,7 +275,7 @@
         }
     }
 
-    private static class SvcParamAlpn extends SvcParam {
+    private static class SvcParamAlpn extends SvcParam<List<String>> {
         private final List<String> mValue;
 
         SvcParamAlpn(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
@@ -281,6 +289,7 @@
             }
         }
 
+        @Override
         List<String> getValue() {
             return Collections.unmodifiableList(mValue);
         }
@@ -291,7 +300,7 @@
         }
     }
 
-    private static class SvcParamNoDefaultAlpn extends SvcParam {
+    private static class SvcParamNoDefaultAlpn extends SvcParam<Void> {
         SvcParamNoDefaultAlpn(@NonNull ByteBuffer buf) throws BufferUnderflowException,
                 ParseException {
             super(KEY_NO_DEFAULT_ALPN);
@@ -303,12 +312,17 @@
         }
 
         @Override
+        Void getValue() {
+            return null;
+        }
+
+        @Override
         public String toString() {
             return toKeyName(getKey());
         }
     }
 
-    private static class SvcParamPort extends SvcParam {
+    private static class SvcParamPort extends SvcParam<Integer> {
         private final int mValue;
 
         SvcParamPort(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
@@ -321,7 +335,8 @@
             mValue = Short.toUnsignedInt(buf.getShort());
         }
 
-        int getValue() {
+        @Override
+        Integer getValue() {
             return mValue;
         }
 
@@ -331,7 +346,7 @@
         }
     }
 
-    private static class SvcParamIpHint extends SvcParam {
+    private static class SvcParamIpHint extends SvcParam<List<InetAddress>> {
         private final List<InetAddress> mValue;
 
         private SvcParamIpHint(int key, @NonNull ByteBuffer buf, int addrLen) throws
@@ -346,6 +361,7 @@
             }
         }
 
+        @Override
         List<InetAddress> getValue() {
             return Collections.unmodifiableList(mValue);
         }
@@ -378,7 +394,7 @@
         }
     }
 
-    private static class SvcParamDohPath extends SvcParam {
+    private static class SvcParamDohPath extends SvcParam<String> {
         private final String mValue;
 
         SvcParamDohPath(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
@@ -390,6 +406,7 @@
             mValue = new String(value, StandardCharsets.UTF_8);
         }
 
+        @Override
         String getValue() {
             return mValue;
         }
@@ -401,7 +418,7 @@
     }
 
     // For other unrecognized and unimplemented SvcParams, they are stored as SvcParamGeneric.
-    private static class SvcParamGeneric extends SvcParam {
+    private static class SvcParamGeneric extends SvcParam<byte[]> {
         private final byte[] mValue;
 
         SvcParamGeneric(int key, @NonNull ByteBuffer buf) throws BufferUnderflowException,
@@ -414,6 +431,12 @@
         }
 
         @Override
+        byte[] getValue() {
+            /* Not yet implemented */
+            return null;
+        }
+
+        @Override
         public String toString() {
             final StringBuilder out = new StringBuilder();
             out.append(toKeyName(getKey()));
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DnsSvcbPacketTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DnsSvcbPacketTest.java
index 6778f8a..d59795f 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/DnsSvcbPacketTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DnsSvcbPacketTest.java
@@ -207,7 +207,7 @@
                 os.write(shortToByteArray((short) mRdataLen));
             } else {
                 final byte[] targetNameLabels =
-                                DnsPacketUtils.DnsRecordParser.domainNameToLabels(mTargetName);
+                        DnsPacketUtils.DnsRecordParser.domainNameToLabels(mTargetName);
                 mRdataLen += (Short.BYTES + targetNameLabels.length);
                 os.write(shortToByteArray((short) mRdataLen));
                 os.write(shortToByteArray(mSvcPriority));
@@ -251,7 +251,9 @@
         // Check the content returned from toString() for now because the getter function for
         // this SvcParam hasn't been implemented.
         // TODO(b/240259333): Consider adding DnsSvcbRecord.isMandatory(String alpn) when needed.
-        assertTrue(record.toString().contains("mandatory=ipv4hint,alpn,key333"));
+        assertTrue(record.toString().contains("ipv4hint"));
+        assertTrue(record.toString().contains("alpn"));
+        assertTrue(record.toString().contains("key333"));
     }
 
     @Test
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index bb32052..198b009 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -62,6 +62,8 @@
 import android.util.Log;
 import android.util.Pair;
 
+import androidx.annotation.Nullable;
+
 import com.android.compatibility.common.util.AmUtils;
 import com.android.compatibility.common.util.BatteryUtils;
 import com.android.compatibility.common.util.DeviceConfigStateHelper;
@@ -283,8 +285,30 @@
     }
 
     protected void assertBackgroundNetworkAccess(boolean expectAllowed) throws Exception {
+        assertBackgroundNetworkAccess(expectAllowed, null);
+    }
+
+    /**
+     * Asserts whether the active network is available or not for the background app. If the network
+     * is unavailable, also checks whether it is blocked by the expected error.
+     *
+     * @param expectAllowed expect background network access to be allowed or not.
+     * @param expectedUnavailableError the expected error when {@code expectAllowed} is false. It's
+     *                                 meaningful only when the {@code expectAllowed} is 'false'.
+     *                                 Throws an IllegalArgumentException when {@code expectAllowed}
+     *                                 is true and this parameter is not null. When the
+     *                                 {@code expectAllowed} is 'false' and this parameter is null,
+     *                                 this function does not compare error type of the networking
+     *                                 access failure.
+     */
+    protected void assertBackgroundNetworkAccess(boolean expectAllowed,
+            @Nullable final String expectedUnavailableError) throws Exception {
         assertBackgroundState();
-        assertNetworkAccess(expectAllowed /* expectAvailable */, false /* needScreenOn */);
+        if (expectAllowed && expectedUnavailableError != null) {
+            throw new IllegalArgumentException("expectedUnavailableError is not null");
+        }
+        assertNetworkAccess(expectAllowed /* expectAvailable */, false /* needScreenOn */,
+                expectedUnavailableError);
     }
 
     protected void assertForegroundNetworkAccess() throws Exception {
@@ -407,12 +431,17 @@
      */
     private void assertNetworkAccess(boolean expectAvailable, boolean needScreenOn)
             throws Exception {
+        assertNetworkAccess(expectAvailable, needScreenOn, null);
+    }
+
+    private void assertNetworkAccess(boolean expectAvailable, boolean needScreenOn,
+            @Nullable final String expectedUnavailableError) throws Exception {
         final int maxTries = 5;
         String error = null;
         int timeoutMs = 500;
 
         for (int i = 1; i <= maxTries; i++) {
-            error = checkNetworkAccess(expectAvailable);
+            error = checkNetworkAccess(expectAvailable, expectedUnavailableError);
 
             if (error == null) return;
 
@@ -479,12 +508,15 @@
      *
      * @return error message with the mismatch (or empty if assertion passed).
      */
-    private String checkNetworkAccess(boolean expectAvailable) throws Exception {
+    private String checkNetworkAccess(boolean expectAvailable,
+            @Nullable final String expectedUnavailableError) throws Exception {
         final String resultData = mServiceClient.checkNetworkStatus();
-        return checkForAvailabilityInResultData(resultData, expectAvailable);
+        return checkForAvailabilityInResultData(resultData, expectAvailable,
+                expectedUnavailableError);
     }
 
-    private String checkForAvailabilityInResultData(String resultData, boolean expectAvailable) {
+    private String checkForAvailabilityInResultData(String resultData, boolean expectAvailable,
+            @Nullable final String expectedUnavailableError) {
         if (resultData == null) {
             assertNotNull("Network status from app2 is null", resultData);
         }
@@ -516,6 +548,10 @@
         if (expectedState != state || expectedDetailedState != detailedState) {
             errors.append(String.format("Connection state mismatch: expected %s/%s, got %s/%s\n",
                     expectedState, expectedDetailedState, state, detailedState));
+        } else if (!expectAvailable && (expectedUnavailableError != null)
+                 && !connectionCheckDetails.contains(expectedUnavailableError)) {
+            errors.append("Connection unavailable reason mismatch: expected "
+                     + expectedUnavailableError + "\n");
         }
 
         if (errors.length() > 0) {
@@ -914,7 +950,7 @@
                 final String resultData = result.get(0).second;
                 if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
                     final String error = checkForAvailabilityInResultData(
-                            resultData, expectAvailable);
+                            resultData, expectAvailable, null /* expectedUnavailableError */);
                     if (error != null) {
                         fail("Network is not available for activity in app2 (" + mUid + "): "
                                 + error);
@@ -949,7 +985,7 @@
                 final String resultData = result.get(0).second;
                 if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
                     final String error = checkForAvailabilityInResultData(
-                            resultData, expectAvailable);
+                            resultData, expectAvailable, null /* expectedUnavailableError */);
                     if (error != null) {
                         Log.d(TAG, "Network state is unexpected, checking again. " + error);
                         // Right now we could end up in an unexpected state if expedited job
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
index ab3cf14..82f4a65 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
@@ -32,8 +32,11 @@
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
+import android.net.cts.util.CtsNetUtils;
 import android.util.Log;
 
+import com.android.modules.utils.build.SdkLevel;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
@@ -46,6 +49,9 @@
 public class NetworkCallbackTest extends AbstractRestrictBackgroundNetworkTestCase {
     private Network mNetwork;
     private final TestNetworkCallback mTestNetworkCallback = new TestNetworkCallback();
+    private CtsNetUtils mCtsNetUtils;
+    private static final String GOOGLE_PRIVATE_DNS_SERVER = "dns.google";
+
     @Rule
     public final MeterednessConfigurationRule mMeterednessConfiguration
             = new MeterednessConfigurationRule();
@@ -218,6 +224,26 @@
         mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
                 false /* hasCapability */, NET_CAPABILITY_NOT_METERED);
         mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
+
+        // Before Android T, DNS queries over private DNS should be but are not restricted by Power
+        // Saver or Data Saver. The issue is fixed in mainline update and apps can no longer request
+        // DNS queries when its network is restricted by Power Saver. The fix takes effect backwards
+        // starting from Android T. But for Data Saver, the fix is not backward compatible since
+        // there are some platform changes involved. It is only available on devices that a specific
+        // trunk flag is enabled.
+        //
+        // This test can not only verify that the network traffic from apps is blocked at the right
+        // time, but also verify whether it is correctly blocked at the DNS stage, or at a later
+        // socket connection stage.
+        if (SdkLevel.isAtLeastT()) {
+            // Enable private DNS
+            mCtsNetUtils = new CtsNetUtils(mContext);
+            mCtsNetUtils.storePrivateDnsSetting();
+            mCtsNetUtils.setPrivateDnsStrictMode(GOOGLE_PRIVATE_DNS_SERVER);
+            mCtsNetUtils.awaitPrivateDnsSetting(
+                    "NetworkCallbackTest wait private DNS setting timeout", mNetwork,
+                    GOOGLE_PRIVATE_DNS_SERVER, true);
+        }
     }
 
     @After
@@ -227,6 +253,10 @@
         setRestrictBackground(false);
         setBatterySaverMode(false);
         unregisterNetworkCallback();
+
+        if (SdkLevel.isAtLeastT() && (mCtsNetUtils != null)) {
+            mCtsNetUtils.restorePrivateDnsSetting();
+        }
     }
 
     @RequiredProperties({DATA_SAVER_MODE})
@@ -235,6 +265,8 @@
         try {
             // Enable restrict background
             setRestrictBackground(true);
+            // TODO: Verify expectedUnavailableError when aconfig support mainline.
+            // (see go/aconfig-in-mainline-problems)
             assertBackgroundNetworkAccess(false);
             assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
             mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
@@ -247,6 +279,7 @@
 
             // Remove from whitelist
             removeRestrictBackgroundWhitelist(mUid);
+            // TODO: Verify expectedUnavailableError when aconfig support mainline.
             assertBackgroundNetworkAccess(false);
             assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
             mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
@@ -278,7 +311,11 @@
         try {
             // Enable Power Saver
             setBatterySaverMode(true);
-            assertBackgroundNetworkAccess(false);
+            if (SdkLevel.isAtLeastT()) {
+                assertBackgroundNetworkAccess(false, "java.net.UnknownHostException");
+            } else {
+                assertBackgroundNetworkAccess(false);
+            }
             mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
             assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
 
@@ -298,7 +335,11 @@
         try {
             // Enable Power Saver
             setBatterySaverMode(true);
-            assertBackgroundNetworkAccess(false);
+            if (SdkLevel.isAtLeastT()) {
+                assertBackgroundNetworkAccess(false, "java.net.UnknownHostException");
+            } else {
+                assertBackgroundNetworkAccess(false);
+            }
             mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
             assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
 
diff --git a/tests/native/utilities/firewall.cpp b/tests/native/utilities/firewall.cpp
index 22f83e8..669b76a 100644
--- a/tests/native/utilities/firewall.cpp
+++ b/tests/native/utilities/firewall.cpp
@@ -59,9 +59,11 @@
 Result<void> Firewall::addRule(uint32_t uid, UidOwnerMatchType match, uint32_t iif) {
     // iif should be non-zero if and only if match == MATCH_IIF
     if (match == IIF_MATCH && iif == 0) {
-        return Errorf("Interface match {} must have nonzero interface index", match);
+        return Errorf("Interface match {} must have nonzero interface index",
+                      static_cast<int>(match));
     } else if (match != IIF_MATCH && iif != 0) {
-        return Errorf("Non-interface match {} must have zero interface index", match);
+        return Errorf("Non-interface match {} must have zero interface index",
+                      static_cast<int>(match));
     }
 
     std::lock_guard guard(mMutex);
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 48cfe77..ff801e5 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -56,11 +56,9 @@
 import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV4_UDP;
 import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV6_ESP;
 import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV6_UDP;
-import static com.android.testutils.Cleanup.testAndCleanup;
 import static com.android.testutils.HandlerUtils.waitForIdleSerialExecutor;
 import static com.android.testutils.MiscAsserts.assertThrows;
 
-import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -149,7 +147,6 @@
 import android.net.wifi.WifiInfo;
 import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
-import android.os.ConditionVariable;
 import android.os.INetworkManagementService;
 import android.os.ParcelFileDescriptor;
 import android.os.PersistableBundle;
@@ -1983,22 +1980,6 @@
         // a subsequent CL.
     }
 
-    @Test
-    public void testStartLegacyVpnIpv6() throws Exception {
-        setMockedUsers(PRIMARY_USER);
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        final LinkProperties lp = new LinkProperties();
-        lp.setInterfaceName(EGRESS_IFACE);
-        lp.addLinkAddress(new LinkAddress("2001:db8::1/64"));
-        final RouteInfo defaultRoute = new RouteInfo(
-                new IpPrefix(Inet6Address.ANY, 0), null, EGRESS_IFACE);
-        lp.addRoute(defaultRoute);
-
-        // IllegalStateException thrown since legacy VPN only supports IPv4.
-        assertThrows(IllegalStateException.class,
-                () -> vpn.startLegacyVpn(mVpnProfile, EGRESS_NETWORK, lp));
-    }
-
     private Vpn startLegacyVpn(final Vpn vpn, final VpnProfile vpnProfile) throws Exception {
         setMockedUsers(PRIMARY_USER);
 
@@ -3112,23 +3093,15 @@
     }
 
     @Test
-    public void testStartRacoonNumericAddress() throws Exception {
-        startRacoon("1.2.3.4", "1.2.3.4");
-    }
+    public void testStartLegacyVpnType() throws Exception {
+        setMockedUsers(PRIMARY_USER);
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
+        final VpnProfile profile = new VpnProfile("testProfile" /* key */);
 
-    @Test
-    public void testStartRacoonHostname() throws Exception {
-        startRacoon("hostname", "5.6.7.8"); // address returned by deps.resolve
-    }
-
-    @Test
-    public void testStartPptp() throws Exception {
-        startPptp(true /* useMppe */);
-    }
-
-    @Test
-    public void testStartPptp_NoMppe() throws Exception {
-        startPptp(false /* useMppe */);
+        profile.type = VpnProfile.TYPE_PPTP;
+        assertThrows(UnsupportedOperationException.class, () -> startLegacyVpn(vpn, profile));
+        profile.type = VpnProfile.TYPE_L2TP_IPSEC_PSK;
+        assertThrows(UnsupportedOperationException.class, () -> startLegacyVpn(vpn, profile));
     }
 
     private void assertTransportInfoMatches(NetworkCapabilities nc, int type) {
@@ -3138,125 +3111,6 @@
         assertEquals(type, ti.getType());
     }
 
-    private void startPptp(boolean useMppe) throws Exception {
-        final VpnProfile profile = new VpnProfile("testProfile" /* key */);
-        profile.type = VpnProfile.TYPE_PPTP;
-        profile.name = "testProfileName";
-        profile.username = "userName";
-        profile.password = "thePassword";
-        profile.server = "192.0.2.123";
-        profile.mppe = useMppe;
-
-        doReturn(new Network[] { new Network(101) }).when(mConnectivityManager).getAllNetworks();
-        doReturn(new Network(102)).when(mConnectivityManager).registerNetworkAgent(
-                any(), // INetworkAgent
-                any(), // NetworkInfo
-                any(), // LinkProperties
-                any(), // NetworkCapabilities
-                any(), // LocalNetworkConfig
-                any(), // NetworkScore
-                any(), // NetworkAgentConfig
-                anyInt()); // provider ID
-
-        final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), profile);
-        final TestDeps deps = (TestDeps) vpn.mDeps;
-
-        testAndCleanup(() -> {
-            final String[] mtpdArgs = deps.mtpdArgs.get(10, TimeUnit.SECONDS);
-            final String[] argsPrefix = new String[]{
-                    EGRESS_IFACE, "pptp", profile.server, "1723", "name", profile.username,
-                    "password", profile.password, "linkname", "vpn", "refuse-eap", "nodefaultroute",
-                    "usepeerdns", "idle", "1800", "mtu", "1270", "mru", "1270"
-            };
-            assertArrayEquals(argsPrefix, Arrays.copyOf(mtpdArgs, argsPrefix.length));
-            if (useMppe) {
-                assertEquals(argsPrefix.length + 2, mtpdArgs.length);
-                assertEquals("+mppe", mtpdArgs[argsPrefix.length]);
-                assertEquals("-pap", mtpdArgs[argsPrefix.length + 1]);
-            } else {
-                assertEquals(argsPrefix.length + 1, mtpdArgs.length);
-                assertEquals("nomppe", mtpdArgs[argsPrefix.length]);
-            }
-
-            verify(mConnectivityManager, timeout(10_000)).registerNetworkAgent(
-                    any(), // INetworkAgent
-                    any(), // NetworkInfo
-                    any(), // LinkProperties
-                    any(), // NetworkCapabilities
-                    any(), // LocalNetworkConfig
-                    any(), // NetworkScore
-                    any(), // NetworkAgentConfig
-                    anyInt()); // provider ID
-        }, () -> { // Cleanup
-                vpn.mVpnRunner.exitVpnRunner();
-                deps.getStateFile().delete(); // set to delete on exit, but this deletes it earlier
-                vpn.mVpnRunner.join(10_000); // wait for up to 10s for the runner to die and cleanup
-            });
-    }
-
-    public void startRacoon(final String serverAddr, final String expectedAddr)
-            throws Exception {
-        final ConditionVariable legacyRunnerReady = new ConditionVariable();
-        final VpnProfile profile = new VpnProfile("testProfile" /* key */);
-        profile.type = VpnProfile.TYPE_L2TP_IPSEC_PSK;
-        profile.name = "testProfileName";
-        profile.username = "userName";
-        profile.password = "thePassword";
-        profile.server = serverAddr;
-        profile.ipsecIdentifier = "id";
-        profile.ipsecSecret = "secret";
-        profile.l2tpSecret = "l2tpsecret";
-
-        when(mConnectivityManager.getAllNetworks())
-            .thenReturn(new Network[] { new Network(101) });
-
-        when(mConnectivityManager.registerNetworkAgent(any(), any(), any(), any(),
-                any(), any(), any(), anyInt())).thenAnswer(invocation -> {
-                    // The runner has registered an agent and is now ready.
-                    legacyRunnerReady.open();
-                    return new Network(102);
-                });
-        final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), profile);
-        final TestDeps deps = (TestDeps) vpn.mDeps;
-        try {
-            // udppsk and 1701 are the values for TYPE_L2TP_IPSEC_PSK
-            assertArrayEquals(
-                    new String[] { EGRESS_IFACE, expectedAddr, "udppsk",
-                            profile.ipsecIdentifier, profile.ipsecSecret, "1701" },
-                    deps.racoonArgs.get(10, TimeUnit.SECONDS));
-            // literal values are hardcoded in Vpn.java for mtpd args
-            assertArrayEquals(
-                    new String[] { EGRESS_IFACE, "l2tp", expectedAddr, "1701", profile.l2tpSecret,
-                            "name", profile.username, "password", profile.password,
-                            "linkname", "vpn", "refuse-eap", "nodefaultroute", "usepeerdns",
-                            "idle", "1800", "mtu", "1270", "mru", "1270" },
-                    deps.mtpdArgs.get(10, TimeUnit.SECONDS));
-
-            // Now wait for the runner to be ready before testing for the route.
-            ArgumentCaptor<LinkProperties> lpCaptor = ArgumentCaptor.forClass(LinkProperties.class);
-            ArgumentCaptor<NetworkCapabilities> ncCaptor =
-                    ArgumentCaptor.forClass(NetworkCapabilities.class);
-            verify(mConnectivityManager, timeout(10_000)).registerNetworkAgent(any(), any(),
-                    lpCaptor.capture(), ncCaptor.capture(), any(), any(), any(), anyInt());
-
-            // In this test the expected address is always v4 so /32.
-            // Note that the interface needs to be specified because RouteInfo objects stored in
-            // LinkProperties objects always acquire the LinkProperties' interface.
-            final RouteInfo expectedRoute = new RouteInfo(new IpPrefix(expectedAddr + "/32"),
-                    null, EGRESS_IFACE, RouteInfo.RTN_THROW);
-            final List<RouteInfo> actualRoutes = lpCaptor.getValue().getRoutes();
-            assertTrue("Expected throw route (" + expectedRoute + ") not found in " + actualRoutes,
-                    actualRoutes.contains(expectedRoute));
-
-            assertTransportInfoMatches(ncCaptor.getValue(), VpnManager.TYPE_VPN_LEGACY);
-        } finally {
-            // Now interrupt the thread, unblock the runner and clean up.
-            vpn.mVpnRunner.exitVpnRunner();
-            deps.getStateFile().delete(); // set to delete on exit, but this deletes it earlier
-            vpn.mVpnRunner.join(10_000); // wait for up to 10s for the runner to die and cleanup
-        }
-    }
-
     // Make it public and un-final so as to spy it
     public class TestDeps extends Vpn.Dependencies {
         public final CompletableFuture<String[]> racoonArgs = new CompletableFuture();
diff --git a/thread/service/java/com/android/server/thread/InfraInterfaceController.java b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
new file mode 100644
index 0000000..d7c49a0
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
@@ -0,0 +1,45 @@
+/*
+ * 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 com.android.server.thread;
+
+import android.os.ParcelFileDescriptor;
+
+import java.io.IOException;
+
+/** Controller for the infrastructure network interface. */
+public class InfraInterfaceController {
+    private static final String TAG = "InfraIfController";
+
+    static {
+        System.loadLibrary("service-thread-jni");
+    }
+
+    /**
+     * Creates a socket on the infrastructure network interface for sending/receiving ICMPv6
+     * Neighbor Discovery messages.
+     *
+     * @param infraInterfaceName the infrastructure network interface name.
+     * @return an ICMPv6 socket file descriptor on the Infrastructure network interface.
+     * @throws IOException when fails to create the socket.
+     */
+    public static ParcelFileDescriptor createIcmp6Socket(String infraInterfaceName)
+            throws IOException {
+        return ParcelFileDescriptor.adoptFd(nativeCreateIcmp6Socket(infraInterfaceName));
+    }
+
+    private static native int nativeCreateIcmp6Socket(String interfaceName) throws IOException;
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index f7872c4..33516aa 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -223,6 +223,7 @@
         return mOtDaemon;
     }
 
+    // TODO(b/309792480): restarts the OT daemon service
     private void onOtDaemonDied() {
         Log.w(TAG, "OT daemon became dead, clean up...");
         OperationReceiverWrapper.onOtDaemonDied();
@@ -568,9 +569,15 @@
     private void handleDeviceRoleChanged(@DeviceRole int deviceRole) {
         if (ThreadNetworkController.isAttached(deviceRole)) {
             Log.d(TAG, "Attached to the Thread network");
+
+            // This is an idempotent method which can be called for multiple times when the device
+            // is already attached (e.g. going from Child to Router)
             registerThreadNetwork();
         } else {
             Log.d(TAG, "Detached from the Thread network");
+
+            // This is an idempotent method which can be called for multiple times when the device
+            // is already detached or stopped
             unregisterThreadNetwork();
         }
     }
@@ -627,7 +634,7 @@
         public void registerStateCallback(IStateCallback callback) {
             checkOnHandlerThread();
             if (mStateCallbacks.containsKey(callback)) {
-                return;
+                throw new IllegalStateException("Registering the same IStateCallback twice");
             }
 
             IBinder.DeathRecipient deathRecipient =
@@ -659,7 +666,8 @@
         public void registerDatasetCallback(IOperationalDatasetCallback callback) {
             checkOnHandlerThread();
             if (mOpDatasetCallbacks.containsKey(callback)) {
-                return;
+                throw new IllegalStateException(
+                        "Registering the same IOperationalDatasetCallback twice");
             }
 
             IBinder.DeathRecipient deathRecipient =
@@ -734,7 +742,7 @@
                 mActiveDataset = newActiveDataset;
             } catch (IllegalArgumentException e) {
                 // Is unlikely that OT will generate invalid Operational Dataset
-                Log.w(TAG, "Ignoring invalid Active Operational Dataset changes", e);
+                Log.wtf(TAG, "Invalid Active Operational Dataset from OpenThread", e);
             }
 
             PendingOperationalDataset newPendingDataset;
@@ -748,7 +756,8 @@
                 onPendingOperationalDatasetChanged(newPendingDataset, listenerId);
                 mPendingDataset = newPendingDataset;
             } catch (IllegalArgumentException e) {
-                Log.w(TAG, "Ignoring invalid Pending Operational Dataset changes", e);
+                // Is unlikely that OT will generate invalid Operational Dataset
+                Log.wtf(TAG, "Invalid Pending Operational Dataset from OpenThread", e);
             }
         }
 
diff --git a/thread/service/java/com/android/server/thread/TunInterfaceController.java b/thread/service/java/com/android/server/thread/TunInterfaceController.java
index ac65b11..7223b2a 100644
--- a/thread/service/java/com/android/server/thread/TunInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/TunInterfaceController.java
@@ -16,6 +16,7 @@
 
 package com.android.server.thread;
 
+import android.annotation.Nullable;
 import android.net.LinkAddress;
 import android.net.util.SocketUtils;
 import android.os.ParcelFileDescriptor;
@@ -34,6 +35,7 @@
 /** Controller for virtual/tunnel network interfaces. */
 public class TunInterfaceController {
     private static final String TAG = "TunIfController";
+    private static final long INFINITE_LIFETIME = 0xffffffffL;
     static final int MTU = 1280;
 
     static {
@@ -76,6 +78,7 @@
     }
 
     /** Returns the FD of the tunnel interface. */
+    @Nullable
     public ParcelFileDescriptor getTunFd() {
         return mParcelTunFd;
     }
@@ -98,7 +101,7 @@
 
         if (address.getDeprecationTime() == LinkAddress.LIFETIME_PERMANENT
                 || address.getDeprecationTime() == LinkAddress.LIFETIME_UNKNOWN) {
-            validLifetimeSeconds = 0xffffffffL;
+            validLifetimeSeconds = INFINITE_LIFETIME;
         } else {
             validLifetimeSeconds =
                     Math.max(
@@ -108,7 +111,7 @@
 
         if (address.getExpirationTime() == LinkAddress.LIFETIME_PERMANENT
                 || address.getExpirationTime() == LinkAddress.LIFETIME_UNKNOWN) {
-            preferredLifetimeSeconds = 0xffffffffL;
+            preferredLifetimeSeconds = INFINITE_LIFETIME;
         } else {
             preferredLifetimeSeconds =
                     Math.max(
diff --git a/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp b/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp
new file mode 100644
index 0000000..5d24eab
--- /dev/null
+++ b/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp
@@ -0,0 +1,141 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "jniThreadInfra"
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <ifaddrs.h>
+#include <inttypes.h>
+#include <linux/if_arp.h>
+#include <linux/ioctl.h>
+#include <log/log.h>
+#include <net/if.h>
+#include <netdb.h>
+#include <netinet/icmp6.h>
+#include <netinet/in.h>
+#include <private/android_filesystem_config.h>
+#include <signal.h>
+#include <spawn.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "jni.h"
+#include "nativehelper/JNIHelp.h"
+#include "nativehelper/scoped_utf_chars.h"
+
+namespace android {
+static jint
+com_android_server_thread_InfraInterfaceController_createIcmp6Socket(JNIEnv *env, jobject clazz,
+                                                                     jstring interfaceName) {
+  ScopedUtfChars ifName(env, interfaceName);
+
+  struct icmp6_filter filter;
+  constexpr int kEnable = 1;
+  constexpr int kIpv6ChecksumOffset = 2;
+  constexpr int kHopLimit = 255;
+
+  // Initializes the ICMPv6 socket.
+  int sock = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
+  if (sock == -1) {
+    jniThrowExceptionFmt(env, "java/io/IOException", "failed to create the socket (%s)",
+                         strerror(errno));
+    return -1;
+  }
+
+  // Only accept Router Advertisements, Router Solicitations and Neighbor
+  // Advertisements.
+  ICMP6_FILTER_SETBLOCKALL(&filter);
+  ICMP6_FILTER_SETPASS(ND_ROUTER_SOLICIT, &filter);
+  ICMP6_FILTER_SETPASS(ND_ROUTER_ADVERT, &filter);
+  ICMP6_FILTER_SETPASS(ND_NEIGHBOR_ADVERT, &filter);
+
+  if (setsockopt(sock, IPPROTO_ICMPV6, ICMP6_FILTER, &filter, sizeof(filter)) != 0) {
+    jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt ICMP6_FILTER (%s)",
+                         strerror(errno));
+    close(sock);
+    return -1;
+  }
+
+  // We want a source address and interface index.
+
+  if (setsockopt(sock, IPPROTO_IPV6, IPV6_RECVPKTINFO, &kEnable, sizeof(kEnable)) != 0) {
+    jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_RECVPKTINFO (%s)",
+                         strerror(errno));
+    close(sock);
+    return -1;
+  }
+
+  if (setsockopt(sock, IPPROTO_RAW, IPV6_CHECKSUM, &kIpv6ChecksumOffset,
+                 sizeof(kIpv6ChecksumOffset)) != 0) {
+    jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_CHECKSUM (%s)",
+                         strerror(errno));
+    close(sock);
+    return -1;
+  }
+
+  // We need to be able to reject RAs arriving from off-link.
+  if (setsockopt(sock, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, &kEnable, sizeof(kEnable)) != 0) {
+    jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_RECVHOPLIMIT (%s)",
+                         strerror(errno));
+    close(sock);
+    return -1;
+  }
+
+  if (setsockopt(sock, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &kHopLimit, sizeof(kHopLimit)) != 0) {
+    jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_UNICAST_HOPS (%s)",
+                         strerror(errno));
+    close(sock);
+    return -1;
+  }
+
+  if (setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &kHopLimit, sizeof(kHopLimit)) != 0) {
+    jniThrowExceptionFmt(env, "java/io/IOException",
+                         "failed to create the setsockopt IPV6_MULTICAST_HOPS (%s)",
+                         strerror(errno));
+    close(sock);
+    return -1;
+  }
+
+  if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, ifName.c_str(), strlen(ifName.c_str()))) {
+    jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt SO_BINDTODEVICE (%s)",
+                         strerror(errno));
+    close(sock);
+    return -1;
+  }
+
+  return sock;
+}
+
+/*
+ * JNI registration.
+ */
+
+static const JNINativeMethod gMethods[] = {
+    /* name, signature, funcPtr */
+    {"nativeCreateIcmp6Socket", "(Ljava/lang/String;)I",
+     (void *)com_android_server_thread_InfraInterfaceController_createIcmp6Socket},
+};
+
+int register_com_android_server_thread_InfraInterfaceController(JNIEnv *env) {
+  return jniRegisterNativeMethods(env, "com/android/server/thread/InfraInterfaceController",
+                                  gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp b/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp
index ed39fab..c56bc0b 100644
--- a/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp
+++ b/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp
@@ -53,25 +53,25 @@
     strlcpy(ifr.ifr_name, ifName.c_str(), sizeof(ifr.ifr_name));
 
     if (ioctl(fd, TUNSETIFF, &ifr, sizeof(ifr)) != 0) {
-        close(fd);
         jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(TUNSETIFF) failed (%s)",
                              strerror(errno));
+        close(fd);
         return -1;
     }
 
     int inet6 = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_IP);
     if (inet6 == -1) {
-        close(fd);
         jniThrowExceptionFmt(env, "java/io/IOException", "create inet6 socket failed (%s)",
                              strerror(errno));
+        close(fd);
         return -1;
     }
     ifr.ifr_mtu = mtu;
     if (ioctl(inet6, SIOCSIFMTU, &ifr) != 0) {
-        close(fd);
-        close(inet6);
         jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(SIOCSIFMTU) failed (%s)",
                              strerror(errno));
+        close(fd);
+        close(inet6);
         return -1;
     }
 
@@ -94,7 +94,6 @@
     }
 
     if (ioctl(inet6, SIOCSIFFLAGS, &ifr) != 0) {
-        close(inet6);
         jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(SIOCSIFFLAGS) failed (%s)",
                              strerror(errno));
     }
diff --git a/thread/service/jni/onload.cpp b/thread/service/jni/onload.cpp
index 5081664..66add74 100644
--- a/thread/service/jni/onload.cpp
+++ b/thread/service/jni/onload.cpp
@@ -19,6 +19,7 @@
 
 namespace android {
 int register_com_android_server_thread_TunInterfaceController(JNIEnv* env);
+int register_com_android_server_thread_InfraInterfaceController(JNIEnv* env);
 }
 
 using namespace android;
@@ -33,5 +34,6 @@
     ALOG_ASSERT(env != NULL, "Could not retrieve the env!");
 
     register_com_android_server_thread_TunInterfaceController(env);
+    register_com_android_server_thread_InfraInterfaceController(env);
     return JNI_VERSION_1_4;
 }