Merge "Update the target SDK version to 30 in the NetworkStackIntegrationTest."
diff --git a/common/moduleutils/src/android/net/ip/ConntrackMonitor.java b/common/moduleutils/src/android/net/ip/ConntrackMonitor.java
index 9189002..6c72984 100644
--- a/common/moduleutils/src/android/net/ip/ConntrackMonitor.java
+++ b/common/moduleutils/src/android/net/ip/ConntrackMonitor.java
@@ -51,6 +51,12 @@
     public static final int NF_NETLINK_CONNTRACK_UPDATE = 2;
     public static final int NF_NETLINK_CONNTRACK_DESTROY = 4;
 
+    // The socket receive buffer size in bytes. If too many conntrack messages are sent too
+    // quickly, the conntrack messages can overflow the socket receive buffer. This can happen
+    // if too many connections are disconnected by losing network and so on. Use a large-enough
+    // buffer to avoid the error ENOBUFS while listening to the conntrack messages.
+    private static final int SOCKET_RECV_BUFSIZE = 6 * 1024 * 1024;
+
     /**
      * A class for describing parsed netfilter conntrack events.
      */
@@ -176,7 +182,7 @@
     public ConntrackMonitor(@NonNull Handler h, @NonNull SharedLog log,
             @NonNull ConntrackEventConsumer cb) {
         super(h, log, TAG, OsConstants.NETLINK_NETFILTER, NF_NETLINK_CONNTRACK_NEW
-                | NF_NETLINK_CONNTRACK_UPDATE | NF_NETLINK_CONNTRACK_DESTROY);
+                | NF_NETLINK_CONNTRACK_UPDATE | NF_NETLINK_CONNTRACK_DESTROY, SOCKET_RECV_BUFSIZE);
         mConsumer = cb;
     }
 
diff --git a/common/moduleutils/src/android/net/ip/NetlinkMonitor.java b/common/moduleutils/src/android/net/ip/NetlinkMonitor.java
index 3d314f1..2025967 100644
--- a/common/moduleutils/src/android/net/ip/NetlinkMonitor.java
+++ b/common/moduleutils/src/android/net/ip/NetlinkMonitor.java
@@ -21,6 +21,8 @@
 import static android.system.OsConstants.AF_NETLINK;
 import static android.system.OsConstants.SOCK_DGRAM;
 import static android.system.OsConstants.SOCK_NONBLOCK;
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_RCVBUF;
 
 import android.annotation.NonNull;
 import android.net.netlink.NetlinkErrorMessage;
@@ -56,9 +58,13 @@
     protected final String mTag;
     private final int mFamily;
     private final int mBindGroups;
+    private final int mSockRcvbufSize;
 
     private static final boolean DBG = false;
 
+    // Default socket receive buffer size. This means the specific buffer size is not set.
+    private static final int DEFAULT_SOCKET_RECV_BUFSIZE = -1;
+
     /**
      * Constructs a new {@code NetlinkMonitor} instance.
      *
@@ -68,14 +74,23 @@
      * @param tag The log tag to use for log messages.
      * @param family the Netlink socket family to, e.g., {@code NETLINK_ROUTE}.
      * @param bindGroups the netlink groups to bind to.
+     * @param sockRcvbufSize the specific socket receive buffer size in bytes. -1 means that don't
+     *        set the specific socket receive buffer size in #createFd and use the default value in
+     *        /proc/sys/net/core/rmem_default file. See SO_RCVBUF in man-pages/socket.
      */
     public NetlinkMonitor(@NonNull Handler h, @NonNull SharedLog log, @NonNull String tag,
-            int family, int bindGroups) {
+            int family, int bindGroups, int sockRcvbufSize) {
         super(h, NetlinkSocket.DEFAULT_RECV_BUFSIZE);
         mLog = log.forSubComponent(tag);
         mTag = tag;
         mFamily = family;
         mBindGroups = bindGroups;
+        mSockRcvbufSize = sockRcvbufSize;
+    }
+
+    public NetlinkMonitor(@NonNull Handler h, @NonNull SharedLog log, @NonNull String tag,
+            int family, int bindGroups) {
+        this(h, log, tag, family, bindGroups, DEFAULT_SOCKET_RECV_BUFSIZE);
     }
 
     @Override
@@ -84,6 +99,9 @@
 
         try {
             fd = Os.socket(AF_NETLINK, SOCK_DGRAM | SOCK_NONBLOCK, mFamily);
+            if (mSockRcvbufSize != DEFAULT_SOCKET_RECV_BUFSIZE) {
+                Os.setsockoptInt(fd, SOL_SOCKET, SO_RCVBUF, mSockRcvbufSize);
+            }
             Os.bind(fd, makeNetlinkSocketAddress(0, mBindGroups));
             NetlinkSocket.connectToKernel(fd);
 
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
index efe1054..b03d653 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -18,6 +18,7 @@
 
 import static android.net.RouteInfo.RTN_UNICAST;
 import static android.net.dhcp.DhcpResultsParcelableUtil.toStableParcelable;
+import static android.net.util.NetworkStackUtils.IPCLIENT_DISABLE_ACCEPT_RA_VERSION;
 import static android.net.util.NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION;
 import static android.net.util.NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION;
 import static android.net.util.SocketUtils.makePacketSocketAddress;
@@ -527,7 +528,7 @@
     private boolean mMulticastFiltering;
     private long mStartTimeMillis;
     private MacAddress mCurrentBssid;
-    private boolean mHasDisabledIPv6OnProvLoss;
+    private boolean mHasDisabledIpv6OrAcceptRaOnProvLoss;
 
     /**
      * Reading the snapshot is an asynchronous operation initiated by invoking
@@ -859,6 +860,33 @@
                 false /* defaultEnabled */);
     }
 
+    private void setInitialBssid(final ProvisioningConfiguration req) {
+        final ScanResultInfo scanResultInfo = req.mScanResultInfo;
+        mCurrentBssid = null;
+        // http://b/185202634
+        // ScanResultInfo is not populated in some situations.
+        // On S and above, prefer getting the BSSID from the Layer2Info.
+        // On R and below, get the BSSID from the ScanResultInfo and fall back to
+        // getting it from the Layer2Info. This ensures no regressions if any R
+        // devices pass in a null or meaningless BSSID in the Layer2Info.
+        if (!ShimUtils.isAtLeastS() && scanResultInfo != null) {
+            try {
+                mCurrentBssid = MacAddress.fromString(scanResultInfo.getBssid());
+            } catch (IllegalArgumentException e) {
+                Log.wtf(mTag, "Invalid BSSID: " + scanResultInfo.getBssid()
+                        + " in provisioning configuration", e);
+            }
+        }
+        if (mCurrentBssid == null && req.mLayer2Info != null) {
+            mCurrentBssid = req.mLayer2Info.mBssid;
+        }
+    }
+
+    private boolean shouldDisableAcceptRaOnProvisioningLoss() {
+        return mDependencies.isFeatureEnabled(mContext, IPCLIENT_DISABLE_ACCEPT_RA_VERSION,
+                true /* defaultEnabled */);
+    }
+
     @Override
     protected void onQuitting() {
         mCallback.onQuit();
@@ -882,17 +910,7 @@
             return;
         }
 
-        final ScanResultInfo scanResultInfo = req.mScanResultInfo;
-        mCurrentBssid = null;
-        if (scanResultInfo != null) {
-            try {
-                mCurrentBssid = MacAddress.fromString(scanResultInfo.getBssid());
-            } catch (IllegalArgumentException e) {
-                Log.wtf(mTag, "Invalid BSSID: " + scanResultInfo.getBssid()
-                        + " in provisioning configuration", e);
-            }
-        }
-
+        setInitialBssid(req);
         if (req.mLayer2Info != null) {
             mL2Key = req.mLayer2Info.mL2Key;
             mCluster = req.mLayer2Info.mCluster;
@@ -1186,6 +1204,21 @@
         return config.isProvisionedBy(lp.getLinkAddresses(), lp.getRoutes());
     }
 
+    private void setIpv6AcceptRa(int acceptRa) {
+        try {
+            mNetd.setProcSysNet(INetd.IPV6, INetd.CONF, mInterfaceParams.name, "accept_ra",
+                    Integer.toString(acceptRa));
+        } catch (Exception e) {
+            Log.e(mTag, "Failed to set accept_ra to " + acceptRa);
+        }
+    }
+
+    private void restartIpv6WithAcceptRaDisabled() {
+        mInterfaceCtrl.disableIPv6();
+        setIpv6AcceptRa(0 /* accept_ra */);
+        startIPv6();
+    }
+
     // TODO: Investigate folding all this into the existing static function
     // LinkProperties.compareProvisioning() or some other single function that
     // takes two LinkProperties objects and returns a ProvisioningChange
@@ -1235,7 +1268,7 @@
         // Note that we can still be disconnected by IpReachabilityMonitor
         // if the IPv6 default gateway (but not the IPv6 DNS servers; see
         // accompanying code in IpReachabilityMonitor) is unreachable.
-        final boolean ignoreIPv6ProvisioningLoss = mHasDisabledIPv6OnProvLoss
+        final boolean ignoreIPv6ProvisioningLoss = mHasDisabledIpv6OrAcceptRaOnProvLoss
                 || (mConfiguration != null && mConfiguration.mUsingMultinetworkPolicyTracker
                         && !mCm.shouldAvoidBadWifi());
 
@@ -1263,18 +1296,31 @@
         if (oldLp.hasGlobalIpv6Address() && (lostIPv6Router && !ignoreIPv6ProvisioningLoss)) {
             // Although link properties have lost IPv6 default route in this case, if IPv4 is still
             // working with appropriate routes and DNS servers, we can keep the current connection
-            // without disconnecting from the network, just disable IPv6 on that given network until
-            // to the next provisioning. Disabling IPv6 will result in all IPv6 connectivity torn
-            // down and all IPv6 sockets being closed, the non-routable IPv6 DNS servers will be
-            // stripped out, so applications will be able to reconnect immediately over IPv4. See
-            // b/131781810.
+            // without disconnecting from the network, just disable IPv6 or accept_ra parameter on
+            // that given network until to the next provisioning.
+            //
+            // Disabling IPv6 stack will result in all IPv6 connectivity torn down and all IPv6
+            // sockets being closed, the non-routable IPv6 DNS servers will be stripped out, so
+            // applications will be able to reconnect immediately over IPv4. See b/131781810.
+            //
+            // Sometimes disabling IPv6 stack might introduce other issues(see b/179222860),
+            // instead disabling accept_ra will result in only IPv4 provisioning and IPv6 link
+            // local address left on the interface, so applications will be able to reconnect
+            // immediately over IPv4 and keep IPv6 link-local capable.
             if (newLp.isIpv4Provisioned()) {
-                mInterfaceCtrl.disableIPv6();
+                if (shouldDisableAcceptRaOnProvisioningLoss()) {
+                    restartIpv6WithAcceptRaDisabled();
+                } else {
+                    mInterfaceCtrl.disableIPv6();
+                }
                 mNetworkQuirkMetrics.setEvent(NetworkQuirkEvent.QE_IPV6_PROVISIONING_ROUTER_LOST);
                 mNetworkQuirkMetrics.statsWrite();
-                mHasDisabledIPv6OnProvLoss = true;
+                mHasDisabledIpv6OrAcceptRaOnProvLoss = true;
                 delta = PROV_CHANGE_STILL_PROVISIONED;
-                mLog.log("Disable IPv6 stack completely when the default router has gone");
+                mLog.log(shouldDisableAcceptRaOnProvisioningLoss()
+                        ? "Disabled accept_ra parameter "
+                        : "Disabled IPv6 stack completely "
+                        + "when the IPv6 default router has gone");
             } else {
                 delta = PROV_CHANGE_LOST_PROVISIONING;
             }
@@ -1827,7 +1873,8 @@
         @Override
         public void enter() {
             stopAllIP();
-            mHasDisabledIPv6OnProvLoss = false;
+            setIpv6AcceptRa(2 /* accept_ra */);
+            mHasDisabledIpv6OrAcceptRaOnProvLoss = false;
             mGratuitousNaTargetAddresses.clear();
 
             mLinkObserver.clearInterfaceParams();
diff --git a/src/android/net/util/NetworkStackUtils.java b/src/android/net/util/NetworkStackUtils.java
index 098ce2e..81d0c08 100755
--- a/src/android/net/util/NetworkStackUtils.java
+++ b/src/android/net/util/NetworkStackUtils.java
@@ -249,6 +249,12 @@
     public static final String IPCLIENT_GARP_NA_ROAMING_VERSION =
             "ipclient_garp_na_roaming_version";
 
+    /**
+     * Experiment flag to disable accept_ra parameter when IPv6 provisioning loss happens due to
+     * the default route has gone.
+     */
+    public static final String IPCLIENT_DISABLE_ACCEPT_RA_VERSION = "ipclient_disable_accept_ra";
+
     static {
         System.loadLibrary("networkstackutilsjni");
     }
diff --git a/tests/integration/src/android/net/ip/IpClientIntegrationTestCommon.java b/tests/integration/src/android/net/ip/IpClientIntegrationTestCommon.java
index c5a8b89..c121b3d 100644
--- a/tests/integration/src/android/net/ip/IpClientIntegrationTestCommon.java
+++ b/tests/integration/src/android/net/ip/IpClientIntegrationTestCommon.java
@@ -782,11 +782,14 @@
     private void startIpClientProvisioning(final boolean isDhcpLeaseCacheEnabled,
             final boolean shouldReplyRapidCommitAck, final boolean isPreconnectionEnabled,
             final boolean isDhcpIpConflictDetectEnabled, final boolean isIPv6OnlyPreferredEnabled,
-            final String displayName, final ScanResultInfo scanResultInfo) throws Exception {
+            final String displayName, final ScanResultInfo scanResultInfo,
+            final Layer2Information layer2Info) throws Exception {
         ProvisioningConfiguration.Builder prov = new ProvisioningConfiguration.Builder()
                 .withoutIpReachabilityMonitor()
-                .withLayer2Information(new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
-                        MacAddress.fromString(TEST_DEFAULT_BSSID)))
+                .withLayer2Information(layer2Info == null
+                        ? new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
+                              MacAddress.fromString(TEST_DEFAULT_BSSID))
+                        : layer2Info)
                 .withoutIPv6();
         if (isPreconnectionEnabled) prov.withPreconnection();
         if (displayName != null) prov.withDisplayName(displayName);
@@ -808,7 +811,7 @@
             throws Exception {
         startIpClientProvisioning(isDhcpLeaseCacheEnabled, isDhcpRapidCommitEnabled,
                 isPreconnectionEnabled, isDhcpIpConflictDetectEnabled, isIPv6OnlyPreferredEnabled,
-                null /* displayName */, null /* ScanResultInfo */);
+                null /* displayName */, null /* ScanResultInfo */, null /* layer2Info */);
     }
 
     private void assertIpMemoryStoreNetworkAttributes(final Integer leaseTimeSec,
@@ -863,10 +866,11 @@
             final boolean isDhcpIpConflictDetectEnabled,
             final boolean isIPv6OnlyPreferredEnabled,
             final String captivePortalApiUrl, final String displayName,
-            final ScanResultInfo scanResultInfo) throws Exception {
+            final ScanResultInfo scanResultInfo, final Layer2Information layer2Info)
+            throws Exception {
         startIpClientProvisioning(isDhcpLeaseCacheEnabled, shouldReplyRapidCommitAck,
                 false /* isPreconnectionEnabled */, isDhcpIpConflictDetectEnabled,
-                isIPv6OnlyPreferredEnabled, displayName, scanResultInfo);
+                isIPv6OnlyPreferredEnabled, displayName, scanResultInfo, layer2Info);
         return handleDhcpPackets(isSuccessLease, leaseTimeSec, shouldReplyRapidCommitAck, mtu,
                 captivePortalApiUrl);
     }
@@ -912,7 +916,8 @@
         return performDhcpHandshake(isSuccessLease, leaseTimeSec, isDhcpLeaseCacheEnabled,
                 isDhcpRapidCommitEnabled, mtu, isDhcpIpConflictDetectEnabled,
                 false /* isIPv6OnlyPreferredEnabled */,
-                null /* captivePortalApiUrl */, null /* displayName */, null /* scanResultInfo */);
+                null /* captivePortalApiUrl */, null /* displayName */, null /* scanResultInfo */,
+                null /* layer2Info */);
     }
 
     private List<DhcpPacket> performDhcpHandshake() throws Exception {
@@ -921,10 +926,21 @@
                 TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
     }
 
-    private DhcpPacket getNextDhcpPacket() throws ParseException {
-        byte[] packet = mDhcpPacketReadHead.getValue().poll(PACKET_TIMEOUT_MS, this::isDhcpPacket);
+    private DhcpPacket getNextDhcpPacket(final long timeout) throws Exception {
+        byte[] packet;
+        while ((packet = mDhcpPacketReadHead.getValue()
+                .poll(timeout, this::isDhcpPacket)) != null) {
+            final DhcpPacket dhcpPacket = DhcpPacket.decodeFullPacket(packet, packet.length,
+                    ENCAP_L2);
+            if (dhcpPacket != null) return dhcpPacket;
+        }
+        return null;
+    }
+
+    private DhcpPacket getNextDhcpPacket() throws Exception {
+        final DhcpPacket packet = getNextDhcpPacket(PACKET_TIMEOUT_MS);
         assertNotNull("No expected DHCP packet received on interface within timeout", packet);
-        return DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L2);
+        return packet;
     }
 
     private DhcpPacket getReplyFromDhcpLease(final NetworkAttributes na, boolean timeout)
@@ -2048,9 +2064,8 @@
         final List<DhcpPacket> sentPackets = performDhcpHandshake(true /* isSuccessLease */,
                 TEST_LEASE_DURATION_S, true /* isDhcpLeaseCacheEnabled */,
                 false /* isDhcpRapidCommitEnabled */, TEST_DEFAULT_MTU,
-                false /* isDhcpIpConflictDetectEnabled */,
-                false /* isIPv6OnlyPreferredEnabled */,
-                null /* captivePortalApiUrl */, null /* displayName */, null /* scanResultInfo */);
+                false /* isDhcpIpConflictDetectEnabled */);
+
         assertEquals(2, sentPackets.size());
         verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
         assertHostname(true, TEST_HOST_NAME, TEST_HOST_NAME_TRANSLITERATION, sentPackets);
@@ -2066,9 +2081,8 @@
         final List<DhcpPacket> sentPackets = performDhcpHandshake(true /* isSuccessLease */,
                 TEST_LEASE_DURATION_S, true /* isDhcpLeaseCacheEnabled */,
                 false /* isDhcpRapidCommitEnabled */, TEST_DEFAULT_MTU,
-                false /* isDhcpIpConflictDetectEnabled */,
-                false /* isIPv6OnlyPreferredEnabled */,
-                null /* captivePortalApiUrl */, null /* displayName */, null /* scanResultInfo */);
+                false /* isDhcpIpConflictDetectEnabled */);
+
         assertEquals(2, sentPackets.size());
         verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
         assertHostname(false, TEST_HOST_NAME, TEST_HOST_NAME_TRANSLITERATION, sentPackets);
@@ -2084,9 +2098,8 @@
         final List<DhcpPacket> sentPackets = performDhcpHandshake(true /* isSuccessLease */,
                 TEST_LEASE_DURATION_S, true /* isDhcpLeaseCacheEnabled */,
                 false /* isDhcpRapidCommitEnabled */, TEST_DEFAULT_MTU,
-                false /* isDhcpIpConflictDetectEnabled */,
-                false /* isIPv6OnlyPreferredEnabled */,
-                null /* captivePortalApiUrl */, null /* displayName */, null /* scanResultInfo */);
+                false /* isDhcpIpConflictDetectEnabled */);
+
         assertEquals(2, sentPackets.size());
         verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
         assertHostname(true, null /* hostname */, null /* hostnameAfterTransliteration */,
@@ -2183,7 +2196,8 @@
                 false /* isDhcpRapidCommitEnabled */, TEST_DEFAULT_MTU,
                 false /* isDhcpIpConflictDetectEnabled */,
                 false /* isIPv6OnlyPreferredEnabled */,
-                null /* captivePortalApiUrl */, displayName, info /* scanResultInfo */);
+                null /* captivePortalApiUrl */, displayName, info /* scanResultInfo */,
+                null /* layer2Info */);
         assertEquals(2, sentPackets.size());
         verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
 
@@ -2270,10 +2284,9 @@
     }
 
     private void doDhcpRoamingTest(final boolean hasMismatchedIpAddress, final String displayName,
-            final String ssid, final String bssid, final boolean expectRoaming) throws Exception {
+            final MacAddress bssid, final boolean expectRoaming) throws Exception {
         long currentTime = System.currentTimeMillis();
-        final ScanResultInfo scanResultInfo = (ssid == null || bssid == null)
-                ? null : makeScanResultInfo(ssid, bssid);
+        final Layer2Information layer2Info = new Layer2Information(TEST_L2KEY, TEST_CLUSTER, bssid);
 
         doAnswer(invocation -> {
             // we don't rely on the Init-Reboot state to renew previous cached IP lease.
@@ -2290,7 +2303,8 @@
                 true /* isDhcpLeaseCacheEnabled */, false /* isDhcpRapidCommitEnabled */,
                 TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */,
                 false /* isIPv6OnlyPreferredEnabled */,
-                null /* captivePortalApiUrl */, displayName, scanResultInfo);
+                null /* captivePortalApiUrl */, displayName, null /* scanResultInfo */,
+                layer2Info);
         verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
         assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
 
@@ -2336,37 +2350,31 @@
     @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
     public void testDhcpRoaming() throws Exception {
         doDhcpRoamingTest(false /* hasMismatchedIpAddress */, "\"0001docomo\"" /* display name */,
-                TEST_DHCP_ROAM_SSID, TEST_DEFAULT_BSSID, true /* expectRoaming */);
+                MacAddress.fromString(TEST_DEFAULT_BSSID), true /* expectRoaming */);
     }
 
     @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
     public void testDhcpRoaming_invalidBssid() throws Exception {
         doDhcpRoamingTest(false /* hasMismatchedIpAddress */, "\"0001docomo\"" /* display name */,
-                TEST_DHCP_ROAM_SSID, TEST_DHCP_ROAM_BSSID, false /* expectRoaming */);
+                MacAddress.fromString(TEST_DHCP_ROAM_BSSID), false /* expectRoaming */);
     }
 
     @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
-    public void testDhcpRoaming_nullScanResultInfo() throws Exception {
+    public void testDhcpRoaming_nullBssid() throws Exception {
         doDhcpRoamingTest(false /* hasMismatchedIpAddress */, "\"0001docomo\"" /* display name */,
-                null /* SSID */, null /* BSSID */, false /* expectRoaming */);
-    }
-
-    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
-    public void testDhcpRoaming_invalidSsid() throws Exception {
-        doDhcpRoamingTest(false /* hasMismatchedIpAddress */, "\"0001docomo\"" /* display name */,
-                TEST_DEFAULT_SSID, TEST_DEFAULT_BSSID, false /* expectRoaming */);
+                null /* BSSID */, false /* expectRoaming */);
     }
 
     @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
     public void testDhcpRoaming_invalidDisplayName() throws Exception {
         doDhcpRoamingTest(false /* hasMismatchedIpAddress */, "\"test-ssid\"" /* display name */,
-                TEST_DHCP_ROAM_SSID, TEST_DEFAULT_BSSID, false /* expectRoaming */);
+                MacAddress.fromString(TEST_DEFAULT_BSSID), false /* expectRoaming */);
     }
 
     @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
     public void testDhcpRoaming_mismatchedLeasedIpAddress() throws Exception {
         doDhcpRoamingTest(true /* hasMismatchedIpAddress */, "\"0001docomo\"" /* display name */,
-                TEST_DHCP_ROAM_SSID, TEST_DEFAULT_BSSID, true /* expectRoaming */);
+                MacAddress.fromString(TEST_DEFAULT_BSSID), true /* expectRoaming */);
     }
 
     private void performDualStackProvisioning() throws Exception {
@@ -2396,12 +2404,15 @@
         reset(mCb);
     }
 
-    private void doDualStackProvisioning() throws Exception {
+    private void doDualStackProvisioning(boolean shouldDisableAcceptRa) throws Exception {
         when(mCm.shouldAvoidBadWifi()).thenReturn(true);
 
         final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                 .withoutIpReachabilityMonitor()
                 .build();
+
+        setFeatureEnabled(NetworkStackUtils.IPCLIENT_DISABLE_ACCEPT_RA_VERSION,
+                shouldDisableAcceptRa);
         // Enable rapid commit to accelerate DHCP handshake to shorten test duration,
         // not strictly necessary.
         setDhcpFeatures(false /* isDhcpLeaseCacheEnabled */, true /* isRapidCommitEnabled */,
@@ -2411,9 +2422,9 @@
         performDualStackProvisioning();
     }
 
-    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
-    public void testIgnoreIpv6ProvisioningLoss() throws Exception {
-        doDualStackProvisioning();
+    @Test @SignatureRequiredTest(reason = "signature perms are required due to mocked callabck")
+    public void testIgnoreIpv6ProvisioningLoss_disableIPv6Stack() throws Exception {
+        doDualStackProvisioning(false /* shouldDisableAcceptRa */);
 
         final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>();
 
@@ -2442,9 +2453,45 @@
                 (long) NetworkQuirkEvent.QE_IPV6_PROVISIONING_ROUTER_LOST.ordinal());
     }
 
+    @Test @SignatureRequiredTest(reason = "signature perms are required due to mocked callabck")
+    public void testIgnoreIpv6ProvisioningLoss_disableAcceptRa() throws Exception {
+        doDualStackProvisioning(true /* shouldDisableAcceptRa */);
+
+        final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>();
+
+        // Send RA with 0-lifetime and wait until all global IPv6 addresses, IPv6-related default
+        // route and DNS servers have been removed, then verify if there is IPv4-only, IPv6 link
+        // local address and route to fe80::/64 info left in the LinkProperties.
+        sendRouterAdvertisementWithZeroLifetime();
+        verify(mCb, timeout(TEST_TIMEOUT_MS).atLeastOnce()).onLinkPropertiesChange(
+                argThat(x -> {
+                    // Only IPv4 provisioned and IPv6 link-local address
+                    final boolean isIPv6LinkLocalAndIPv4OnlyProvisioned =
+                            (x.getLinkAddresses().size() == 2
+                                    && x.getDnsServers().size() == 1
+                                    && x.getAddresses().get(0) instanceof Inet4Address
+                                    && x.getDnsServers().get(0) instanceof Inet4Address);
+
+                    if (!isIPv6LinkLocalAndIPv4OnlyProvisioned) return false;
+                    lpFuture.complete(x);
+                    return true;
+                }));
+        final LinkProperties lp = lpFuture.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        assertNotNull(lp);
+        assertEquals(lp.getAddresses().get(0), CLIENT_ADDR);
+        assertEquals(lp.getDnsServers().get(0), SERVER_ADDR);
+        assertTrue(lp.getAddresses().get(1).isLinkLocalAddress());
+
+        reset(mCb);
+
+        // Send an RA to verify that global IPv6 addresses won't be configured on the interface.
+        sendBasicRouterAdvertisement(false /* waitForRs */);
+        verify(mCb, timeout(TEST_TIMEOUT_MS).times(0)).onLinkPropertiesChange(any());
+    }
+
     @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
     public void testDualStackProvisioning() throws Exception {
-        doDualStackProvisioning();
+        doDualStackProvisioning(false /* shouldDisableAcceptRa */);
 
         verify(mCb, never()).onProvisioningFailure(any());
     }
@@ -2664,7 +2711,7 @@
     public void testNoFdLeaks() throws Exception {
         // Shut down and restart IpClient once to ensure that any fds that are opened the first
         // time it runs do not cause the test to fail.
-        doDualStackProvisioning();
+        doDualStackProvisioning(false /* shouldDisableAcceptRa */);
         shutdownAndRecreateIpClient();
 
         // Unfortunately we cannot use a large number of iterations as it would make the test run
@@ -2672,7 +2719,7 @@
         final int iterations = 10;
         final int before = getNumOpenFds();
         for (int i = 0; i < iterations; i++) {
-            doDualStackProvisioning();
+            doDualStackProvisioning(false /* shouldDisableAcceptRa */);
             shutdownAndRecreateIpClient();
             // The last time this loop runs, mIpc will be shut down in tearDown.
         }
@@ -2860,10 +2907,13 @@
 
     private void startGratuitousArpAndNaAfterRoamingTest(boolean isGratuitousArpNaRoamingEnabled,
             boolean hasIpv4, boolean hasIpv6) throws Exception {
+        final Layer2Information layer2Info = new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
+                MacAddress.fromString(TEST_DEFAULT_BSSID));
         final ScanResultInfo scanResultInfo =
                 makeScanResultInfo(TEST_DEFAULT_SSID, TEST_DEFAULT_BSSID);
         final ProvisioningConfiguration.Builder prov = new ProvisioningConfiguration.Builder()
                 .withoutIpReachabilityMonitor()
+                .withLayer2Information(layer2Info)
                 .withScanResultInfo(scanResultInfo)
                 .withDisplayName("ssid");
         if (!hasIpv4) prov.withoutIPv4();
@@ -2957,4 +3007,119 @@
         assertEquals(0, naList.size());
         assertEquals(1, arpList.size());
     }
+
+    private void doInitialBssidSetupTest(final Layer2Information layer2Info,
+            final ScanResultInfo scanResultInfo) throws Exception {
+        ProvisioningConfiguration.Builder prov = new ProvisioningConfiguration.Builder()
+                .withoutIpReachabilityMonitor()
+                .withLayer2Information(layer2Info)
+                .withScanResultInfo(scanResultInfo)
+                .withDisplayName("\"0001docomo\"")
+                .withoutIPv6();
+
+        setDhcpFeatures(false /* isDhcpLeaseCacheEnabled */, true /* shouldReplyRapidCommitAck */,
+                false /* isDhcpIpConflictDetectEnabled */, false /* isIPv6OnlyPreferredEnabled */);
+        startIpClientProvisioning(prov.build());
+
+        handleDhcpPackets(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
+                true /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU, null /* serverSentUrl */);
+        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
+        forceLayer2Roaming();
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testSetInitialBssidFromLayer2Info() throws Exception {
+        final Layer2Information layer2Info = new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
+                MacAddress.fromString(TEST_DEFAULT_BSSID));
+
+        doInitialBssidSetupTest(layer2Info, null /* scanResultInfo */);
+
+        // Initial BSSID comes from layer2Info, it's different with target roaming bssid,
+        // then verify that DHCPREQUEST packet is sent after roaming.
+        final DhcpPacket packet = getNextDhcpPacket();
+        assertTrue(packet instanceof DhcpRequestPacket);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testSetInitialBssidFromLayer2Info_NullBssid() throws Exception {
+        final Layer2Information layer2Info = new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
+                null /* bssid */);
+        final ScanResultInfo scanResultInfo =
+                makeScanResultInfo(TEST_DEFAULT_SSID, TEST_DHCP_ROAM_BSSID);
+
+        doInitialBssidSetupTest(layer2Info, scanResultInfo);
+
+        // Initial BSSID comes from layer2Info, it's null, no DHCPREQUEST packet
+        // will be sent after roaming.
+        final DhcpPacket packet = getNextDhcpPacket(TEST_TIMEOUT_MS);
+        assertNull(packet);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testSetInitialBssidFromLayer2Info_SameRoamingBssid() throws Exception {
+        final Layer2Information layer2Info = new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
+                MacAddress.fromString(TEST_DHCP_ROAM_BSSID));
+
+        doInitialBssidSetupTest(layer2Info, null /* scanResultInfo */);
+
+        // Initial BSSID comes from layer2Info, it's same with target roaming bssid,
+        // no DHCPREQUEST packet will be sent after roaming.
+        final DhcpPacket packet = getNextDhcpPacket(TEST_TIMEOUT_MS);
+        assertNull(packet);
+    }
+
+    @Test @IgnoreAfter(Build.VERSION_CODES.R)
+    public void testSetInitialBssidFromScanResultInfo() throws Exception {
+        final ScanResultInfo scanResultInfo =
+                makeScanResultInfo(TEST_DEFAULT_SSID, TEST_DEFAULT_BSSID);
+
+        doInitialBssidSetupTest(null /* layer2Info */, scanResultInfo);
+
+        // Initial BSSID comes from ScanResultInfo, it's different with target roaming bssid,
+        // then verify that DHCPREQUEST packet is sent after roaming.
+        final DhcpPacket packet = getNextDhcpPacket();
+        assertTrue(packet instanceof DhcpRequestPacket);
+    }
+
+    @Test @IgnoreAfter(Build.VERSION_CODES.R)
+    public void testSetInitialBssidFromScanResultInfo_SameRoamingBssid() throws Exception {
+        final ScanResultInfo scanResultInfo =
+                makeScanResultInfo(TEST_DEFAULT_SSID, TEST_DHCP_ROAM_BSSID);
+
+        doInitialBssidSetupTest(null /* layer2Info */, scanResultInfo);
+
+        // Initial BSSID comes from ScanResultInfo, it's same with target roaming bssid,
+        // no DHCPREQUEST packet will be sent after roaming.
+        final DhcpPacket packet = getNextDhcpPacket(TEST_TIMEOUT_MS);
+        assertNull(packet);
+    }
+
+    @Test @IgnoreAfter(Build.VERSION_CODES.R)
+    public void testSetInitialBssidFromScanResultInfo_BrokenInitialBssid() throws Exception {
+        final ScanResultInfo scanResultInfo =
+                makeScanResultInfo(TEST_DEFAULT_SSID, "00:11:22:33:44:");
+
+        doInitialBssidSetupTest(null /* layer2Info */, scanResultInfo);
+
+        // Initial BSSID comes from ScanResultInfo, it's broken MAC address format and fallback
+        // to null layer2Info, no DHCPREQUEST packet will be sent after roaming.
+        final DhcpPacket packet = getNextDhcpPacket(TEST_TIMEOUT_MS);
+        assertNull(packet);
+    }
+
+    @Test @IgnoreAfter(Build.VERSION_CODES.R)
+    public void testSetInitialBssidFromScanResultInfo_BrokenInitialBssidFallback()
+            throws Exception {
+        final Layer2Information layer2Info = new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
+                MacAddress.fromString(TEST_DEFAULT_BSSID));
+        final ScanResultInfo scanResultInfo =
+                makeScanResultInfo(TEST_DEFAULT_SSID, "00:11:22:33:44:");
+
+        doInitialBssidSetupTest(layer2Info, scanResultInfo);
+
+        // Initial BSSID comes from ScanResultInfo, it's broken MAC address format and fallback
+        // to check layer2Info, then verify DHCPREQUEST packet will be sent after roaming.
+        final DhcpPacket packet = getNextDhcpPacket();
+        assertTrue(packet instanceof DhcpRequestPacket);
+    }
 }