Add basic IpReachabilityMonitor integration test. am: 60175d2d5e am: 368a7117ff

Original change: https://android-review.googlesource.com/c/platform/packages/modules/NetworkStack/+/1724690

Change-Id: I4dae1d01ba9c23b23deb7ddeb23b4299f8671727
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
index 445f915..c0ae611 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -70,6 +70,7 @@
 import android.net.util.SharedLog;
 import android.os.Build;
 import android.os.ConditionVariable;
+import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
 import android.os.RemoteException;
@@ -596,11 +597,33 @@
             return new IpConnectivityLog();
         }
 
+        /**
+         * Get a NetworkQuirkMetrics instance.
+         */
         public NetworkQuirkMetrics getNetworkQuirkMetrics() {
             return new NetworkQuirkMetrics();
         }
 
         /**
+         * Get a IpReachabilityMonitor instance.
+         */
+        public IpReachabilityMonitor getIpReachabilityMonitor(Context context,
+                InterfaceParams ifParams, Handler h, SharedLog log,
+                IpReachabilityMonitor.Callback callback, boolean usingMultinetworkPolicyTracker,
+                IpReachabilityMonitor.Dependencies deps, final INetd netd) {
+            return new IpReachabilityMonitor(context, ifParams, h, log, callback,
+                    usingMultinetworkPolicyTracker, deps, netd);
+        }
+
+        /**
+         * Get a IpReachabilityMonitor dependencies instance.
+         */
+        public IpReachabilityMonitor.Dependencies getIpReachabilityMonitorDeps(Context context,
+                String name) {
+            return IpReachabilityMonitor.Dependencies.makeDefault(context, name);
+        }
+
+        /**
          * Return whether a feature guarded by a feature flag is enabled.
          * @see NetworkStackUtils#isFeatureEnabled(Context, String, String)
          */
@@ -1776,7 +1799,7 @@
 
     private boolean startIpReachabilityMonitor() {
         try {
-            mIpReachabilityMonitor = new IpReachabilityMonitor(
+            mIpReachabilityMonitor = mDependencies.getIpReachabilityMonitor(
                     mContext,
                     mInterfaceParams,
                     getHandler(),
@@ -1788,6 +1811,7 @@
                         }
                     },
                     mConfiguration.mUsingMultinetworkPolicyTracker,
+                    mDependencies.getIpReachabilityMonitorDeps(mContext, mInterfaceParams.name),
                     mNetd);
         } catch (IllegalArgumentException iae) {
             // Failed to start IpReachabilityMonitor. Log it and call
diff --git a/src/android/net/ip/IpReachabilityMonitor.java b/src/android/net/ip/IpReachabilityMonitor.java
index 3dbe662..face72c 100644
--- a/src/android/net/ip/IpReachabilityMonitor.java
+++ b/src/android/net/ip/IpReachabilityMonitor.java
@@ -199,9 +199,9 @@
 
     public IpReachabilityMonitor(
             Context context, InterfaceParams ifParams, Handler h, SharedLog log, Callback callback,
-            boolean usingMultinetworkPolicyTracker, final INetd netd) {
-        this(context, ifParams, h, log, callback, usingMultinetworkPolicyTracker,
-                Dependencies.makeDefault(context, ifParams.name), new IpConnectivityLog(), netd);
+            boolean usingMultinetworkPolicyTracker, Dependencies dependencies, final INetd netd) {
+        this(context, ifParams, h, log, callback, usingMultinetworkPolicyTracker, dependencies,
+                new IpConnectivityLog(), netd);
     }
 
     @VisibleForTesting
diff --git a/tests/integration/src/android/net/ip/IpClientIntegrationTest.kt b/tests/integration/src/android/net/ip/IpClientIntegrationTest.kt
index eb0a799..b217ebb 100644
--- a/tests/integration/src/android/net/ip/IpClientIntegrationTest.kt
+++ b/tests/integration/src/android/net/ip/IpClientIntegrationTest.kt
@@ -18,6 +18,8 @@
 
 import android.net.ipmemorystore.NetworkAttributes
 import android.util.ArrayMap
+import java.net.Inet6Address
+import kotlin.test.assertEquals
 import org.mockito.Mockito.any
 import org.mockito.ArgumentCaptor
 import org.mockito.Mockito.eq
@@ -56,4 +58,15 @@
     override fun assertIpMemoryNeverStoreNetworkAttributes(l2Key: String, timeout: Long) {
         verify(mIpMemoryStore, never()).storeNetworkAttributes(eq(l2Key), any(), any())
     }
+
+    override fun assertNotifyNeighborLost(targetIp: Inet6Address) {
+        val target = ArgumentCaptor.forClass(Inet6Address::class.java)
+
+        verify(mCallback, timeout(TEST_TIMEOUT_MS)).notifyLost(target.capture(), any())
+        assertEquals(targetIp, target.getValue())
+    }
+
+    override fun assertNeverNotifyNeighborLost() {
+        verify(mCallback, never()).notifyLost(any(), any())
+    }
 }
diff --git a/tests/integration/src/android/net/ip/IpClientIntegrationTestCommon.java b/tests/integration/src/android/net/ip/IpClientIntegrationTestCommon.java
index e40db77..630b05d 100644
--- a/tests/integration/src/android/net/ip/IpClientIntegrationTestCommon.java
+++ b/tests/integration/src/android/net/ip/IpClientIntegrationTestCommon.java
@@ -40,13 +40,17 @@
 import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT;
+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.IPV4_ADDR_ANY;
 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.IPV6_HEADER_LEN;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_PROTOCOL_OFFSET;
+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;
 
@@ -108,6 +112,7 @@
 import android.net.dhcp.DhcpPacket;
 import android.net.dhcp.DhcpPacket.ParseException;
 import android.net.dhcp.DhcpRequestPacket;
+import android.net.ip.IpNeighborMonitor.NeighborEventConsumer;
 import android.net.ipmemorystore.NetworkAttributes;
 import android.net.ipmemorystore.OnNetworkAttributesRetrievedListener;
 import android.net.ipmemorystore.Status;
@@ -119,6 +124,7 @@
 import android.net.shared.ProvisioningConfiguration.ScanResultInfo;
 import android.net.util.InterfaceParams;
 import android.net.util.NetworkStackUtils;
+import android.net.util.SharedLog;
 import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -141,6 +147,7 @@
 import com.android.internal.util.StateMachine;
 import com.android.net.module.util.ArrayTrackRecord;
 import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.structs.LlaOption;
 import com.android.net.module.util.structs.PrefixInformationOption;
 import com.android.net.module.util.structs.RdnssOption;
 import com.android.networkstack.apishim.CaptivePortalDataShimImpl;
@@ -150,6 +157,7 @@
 import com.android.networkstack.metrics.IpProvisioningMetrics;
 import com.android.networkstack.metrics.NetworkQuirkMetrics;
 import com.android.networkstack.packets.NeighborAdvertisement;
+import com.android.networkstack.packets.NeighborSolicitation;
 import com.android.server.NetworkObserver;
 import com.android.server.NetworkObserverRegistry;
 import com.android.server.NetworkStackService.NetworkStackServiceManager;
@@ -263,6 +271,7 @@
     @Mock private PowerManager.WakeLock mTimeoutWakeLock;
     @Mock protected NetworkStackIpMemoryStore mIpMemoryStore;
     @Mock private NetworkQuirkMetrics.Dependencies mNetworkQuirkMetricsDeps;
+    @Mock protected IpReachabilityMonitor.Callback mCallback;
 
     @Spy private INetd mNetd;
     private NetworkObserverRegistry mNetworkObserverRegistry;
@@ -272,7 +281,7 @@
 
     /***** END signature required test members *****/
 
-    private IIpClientCallbacks mCb;
+    protected IIpClientCallbacks mCb;
     private IIpClient mIIpClient;
     private String mIfaceName;
     private HandlerThread mPacketReaderThread;
@@ -327,7 +336,10 @@
     private static final String HOSTNAME = "testhostname";
     private static final int TEST_DEFAULT_MTU = 1500;
     private static final int TEST_MIN_MTU = 1280;
-    private static final byte[] SERVER_MAC = new byte[] { 0x00, 0x1A, 0x11, 0x22, 0x33, 0x44 };
+    private static final MacAddress ROUTER_MAC = MacAddress.fromString("00:1A:11:22:33:44");
+    private static final byte[] ROUTER_MAC_BYTES = ROUTER_MAC.toByteArray();
+    private static final Inet6Address ROUTER_LINK_LOCAL =
+                (Inet6Address) InetAddresses.parseNumericAddress("fe80::1");
     private static final String TEST_HOST_NAME = "AOSP on Crosshatch";
     private static final String TEST_HOST_NAME_TRANSLITERATION = "AOSP-on-Crosshatch";
     private static final String TEST_CAPTIVE_PORTAL_URL = "https://example.com/capportapi";
@@ -401,6 +413,15 @@
         }
 
         @Override
+        public IpReachabilityMonitor getIpReachabilityMonitor(Context context,
+                InterfaceParams ifParams, Handler h, SharedLog log,
+                IpReachabilityMonitor.Callback callback, boolean usingMultinetworkPolicyTracker,
+                IpReachabilityMonitor.Dependencies deps, final INetd netd) {
+            return new IpReachabilityMonitor(context, ifParams, h, log, mCallback,
+                    usingMultinetworkPolicyTracker, deps, netd);
+        }
+
+        @Override
         public boolean isFeatureEnabled(final Context context, final String name,
                 final boolean defaultEnabled) {
             return IpClientIntegrationTestCommon.this.isFeatureEnabled(name, defaultEnabled);
@@ -440,6 +461,23 @@
         }
 
         @Override
+        public IpReachabilityMonitor.Dependencies getIpReachabilityMonitorDeps(Context context,
+                String name) {
+            return new IpReachabilityMonitor.Dependencies() {
+                public void acquireWakeLock(long durationMs) {
+                    // It doesn't matter for the integration test app on whether the wake lock
+                    // is acquired or not.
+                    return;
+                }
+
+                public IpNeighborMonitor makeIpNeighborMonitor(Handler h, SharedLog log,
+                        NeighborEventConsumer cb) {
+                    return new IpNeighborMonitor(h, log, cb);
+                }
+            };
+        }
+
+        @Override
         public int getDeviceConfigPropertyInt(String name, int defaultValue) {
             Integer value = mIntConfigProperties.get(name);
             if (value == null) {
@@ -472,6 +510,10 @@
 
     protected abstract void assertIpMemoryNeverStoreNetworkAttributes(String l2Key, long timeout);
 
+    protected abstract void assertNotifyNeighborLost(Inet6Address targetIp);
+
+    protected abstract void assertNeverNotifyNeighborLost();
+
     protected final boolean testSkipped() {
         // TODO: split out a test suite for root tests, and fail hard instead of skipping the test
         // if it is run on devices where TestNetworkStackServiceClient is not supported
@@ -712,6 +754,14 @@
         }
     }
 
+    private NeighborSolicitation parseNeighborSolicitationOrNull(final byte[] packet) {
+        try {
+            return NeighborSolicitation.parse(packet, packet.length);
+        } catch (NeighborSolicitation.ParseException e) {
+            return null;
+        }
+    }
+
     private static ByteBuffer buildDhcpOfferPacket(final DhcpPacket packet,
             final Inet4Address clientAddress, final Integer leaseTimeSec, final short mtu,
             final String captivePortalUrl, final Integer ipv6OnlyWaitTime) {
@@ -761,7 +811,7 @@
 
     private void sendArpReply(final byte[] clientMac) throws IOException {
         final ByteBuffer packet = ArpPacket.buildArpPacket(clientMac /* dst */,
-                SERVER_MAC /* src */, INADDR_ANY.getAddress() /* target IP */,
+                ROUTER_MAC_BYTES /* srcMac */, INADDR_ANY.getAddress() /* target IP */,
                 clientMac /* target HW address */, CLIENT_ADDR.getAddress() /* sender IP */,
                 (short) ARP_REPLY);
         mPacketReader.sendResponse(packet);
@@ -769,7 +819,7 @@
 
     private void sendArpProbe() throws IOException {
         final ByteBuffer packet = ArpPacket.buildArpPacket(DhcpPacket.ETHER_BROADCAST /* dst */,
-                SERVER_MAC /* src */, CLIENT_ADDR.getAddress() /* target IP */,
+                ROUTER_MAC_BYTES /* srcMac */, CLIENT_ADDR.getAddress() /* target IP */,
                 new byte[ETHER_ADDR_LEN] /* target HW address */,
                 INADDR_ANY.getAddress() /* sender IP */, (short) ARP_REQUEST);
         mPacketReader.sendResponse(packet);
@@ -1458,20 +1508,23 @@
         HandlerUtils.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);
     }
 
-    private boolean isRouterSolicitation(final byte[] packetBytes) {
+    private boolean isIcmpv6PacketOfType(final byte[] packetBytes, int type) {
         ByteBuffer packet = ByteBuffer.wrap(packetBytes);
         return packet.getShort(ETHER_TYPE_OFFSET) == (short) ETH_P_IPV6
                 && packet.get(ETHER_HEADER_LEN + IPV6_PROTOCOL_OFFSET) == (byte) IPPROTO_ICMPV6
-                && packet.get(ETHER_HEADER_LEN + IPV6_HEADER_LEN)
-                        == (byte) ICMPV6_ROUTER_SOLICITATION;
+                && packet.get(ETHER_HEADER_LEN + IPV6_HEADER_LEN) == (byte) type;
+    }
+
+    private boolean isRouterSolicitation(final byte[] packetBytes) {
+        return isIcmpv6PacketOfType(packetBytes, ICMPV6_ROUTER_SOLICITATION);
     }
 
     private boolean isNeighborAdvertisement(final byte[] packetBytes) {
-        ByteBuffer packet = ByteBuffer.wrap(packetBytes);
-        return packet.getShort(ETHER_TYPE_OFFSET) == (short) ETH_P_IPV6
-                && packet.get(ETHER_HEADER_LEN + IPV6_PROTOCOL_OFFSET) == (byte) IPPROTO_ICMPV6
-                && packet.get(ETHER_HEADER_LEN + IPV6_HEADER_LEN)
-                        == (byte) ICMPV6_NEIGHBOR_ADVERTISEMENT;
+        return isIcmpv6PacketOfType(packetBytes, ICMPV6_NEIGHBOR_ADVERTISEMENT);
+    }
+
+    private boolean isNeighborSolicitation(final byte[] packetBytes) {
+        return isIcmpv6PacketOfType(packetBytes, ICMPV6_NEIGHBOR_SOLICITATION);
     }
 
     private NeighborAdvertisement getNextNeighborAdvertisement() throws ParseException {
@@ -1484,6 +1537,16 @@
         return na;
     }
 
+    private NeighborSolicitation getNextNeighborSolicitation() throws ParseException {
+        final byte[] packet = mPacketReader.popPacket(PACKET_TIMEOUT_MS,
+                this::isNeighborSolicitation);
+        if (packet == null) return null;
+
+        final NeighborSolicitation ns = parseNeighborSolicitationOrNull(packet);
+        assertNotNull("Invalid neighbour solicitation received", ns);
+        return ns;
+    }
+
     private void waitForRouterSolicitation() throws ParseException {
         assertNotNull("No router solicitation received on interface within timeout",
                 mPacketReader.popPacket(PACKET_TIMEOUT_MS, this::isRouterSolicitation));
@@ -1521,17 +1584,18 @@
         return RdnssOption.build(lifetime, servers);
     }
 
+    private static ByteBuffer buildSllaOption() throws Exception {
+        return LlaOption.build((byte) ICMPV6_ND_OPTION_SLLA, ROUTER_MAC);
+    }
+
     private static ByteBuffer buildRaPacket(short lifetime, ByteBuffer... options)
             throws Exception {
-        final MacAddress dstMac = MacAddress.fromString("33:33:00:00:00:01");
-        final MacAddress srcMac = MacAddress.fromString("01:02:03:04:05:06");
-        final Inet6Address routerLinkLocal =
-                (Inet6Address) InetAddresses.parseNumericAddress("fe80::1");
-
-        return Ipv6Utils.buildRaPacket(srcMac, dstMac, routerLinkLocal,
-                IPV6_ADDR_ALL_NODES_MULTICAST, (byte) 0 /* M=0, O=0 */, lifetime,
-                0 /* Reachable time, unspecified */, 100 /* Retrans time 100ms */,
-                options);
+        final MacAddress dstMac =
+                NetworkStackUtils.ipv6MulticastToEthernetMulticast(IPV6_ADDR_ALL_ROUTERS_MULTICAST);
+        return Ipv6Utils.buildRaPacket(ROUTER_MAC /* srcMac */, dstMac,
+                ROUTER_LINK_LOCAL /* srcIp */, IPV6_ADDR_ALL_NODES_MULTICAST /* dstIp */,
+                (byte) 0 /* M=0, O=0 */, lifetime, 0 /* Reachable time, unspecified */,
+                100 /* Retrans time 100ms */, options);
     }
 
     private static ByteBuffer buildRaPacket(ByteBuffer... options) throws Exception {
@@ -1575,7 +1639,8 @@
         final String dnsServer = "2001:4860:4860::64";
         final ByteBuffer pio = buildPioOption(3600, 1800, "2001:db8:1::/64");
         final ByteBuffer rdnss = buildRdnssOption(3600, dnsServer);
-        final ByteBuffer ra = buildRaPacket(pio, rdnss);
+        final ByteBuffer slla = buildSllaOption();
+        final ByteBuffer ra = buildRaPacket(pio, rdnss, slla);
 
         return doIpv6OnlyProvisioning(inOrder, ra);
     }
@@ -2383,7 +2448,8 @@
         final String dnsServer = "2001:4860:4860::64";
         final ByteBuffer pio = buildPioOption(3600, 1800, "2001:db8:1::/64");
         final ByteBuffer rdnss = buildRdnssOption(3600, dnsServer);
-        final ByteBuffer ra = buildRaPacket(pio, rdnss);
+        final ByteBuffer slla = buildSllaOption();
+        final ByteBuffer ra = buildRaPacket(pio, rdnss, slla);
 
         doIpv6OnlyProvisioning(inOrder, ra);
 
@@ -2965,7 +3031,7 @@
     @Test
     public void testGratuitousArpAndNaAfterRoaming_disableExpFlag() throws Exception {
         startGratuitousArpAndNaAfterRoamingTest(false /* isGratuitousArpNaRoamingEnabled */,
-                true /* hasIpv6 */, true /* hasIpv6 */);
+                true /* hasIpv4 */, true /* hasIpv6 */);
         performDualStackProvisioning();
         forceLayer2Roaming();
 
@@ -3007,4 +3073,84 @@
         assertEquals(0, naList.size());
         assertEquals(1, arpList.size());
     }
+
+    private void assertNeighborSolicitation(final NeighborSolicitation ns,
+            final Inet6Address target) {
+        assertEquals(ETH_P_IPV6, ns.ethHdr.etherType);
+        assertEquals(IPPROTO_ICMPV6, ns.ipv6Hdr.nextHeader);
+        assertEquals(0xff, ns.ipv6Hdr.hopLimit);
+        assertTrue(ns.ipv6Hdr.srcIp.isLinkLocalAddress());
+        assertEquals(ICMPV6_NEIGHBOR_SOLICITATION, ns.icmpv6Hdr.type);
+        assertEquals(0, ns.icmpv6Hdr.code);
+        assertEquals(0, ns.nsHdr.reserved);
+        assertEquals(target, ns.nsHdr.target);
+        assertEquals(ns.slla.linkLayerAddress, ns.ethHdr.srcMac);
+    }
+
+    private void assertUnicastNeighborSolicitation(final NeighborSolicitation ns,
+            final MacAddress dstMac, final Inet6Address dstIp, final Inet6Address target) {
+        assertEquals(dstMac, ns.ethHdr.dstMac);
+        assertEquals(dstIp, ns.ipv6Hdr.dstIp);
+        assertNeighborSolicitation(ns, target);
+    }
+
+    private void prepareIpReachabilityMonitorTest() throws Exception {
+        final ScanResultInfo info = makeScanResultInfo(TEST_DEFAULT_SSID, TEST_DEFAULT_BSSID);
+        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withLayer2Information(new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
+                       MacAddress.fromString(TEST_DEFAULT_BSSID)))
+                .withScanResultInfo(info)
+                .withDisplayName(TEST_DEFAULT_SSID)
+                .withoutIPv4()
+                .build();
+        startIpClientProvisioning(config);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(false);
+        doIpv6OnlyProvisioning();
+
+        // Simulate the roaming.
+        forceLayer2Roaming();
+    }
+
+    @Test
+    public void testIpReachabilityMonitor_probeFailed() throws Exception {
+        prepareIpReachabilityMonitorTest();
+
+        NeighborSolicitation packet;
+        final List<NeighborSolicitation> nsList = new ArrayList<NeighborSolicitation>();
+        while ((packet = getNextNeighborSolicitation()) != null) {
+            // Filter out the NSes used for duplicate address detetction, the target address
+            // is the global IPv6 address inside these NSes.
+            if (packet.nsHdr.target.isLinkLocalAddress()) {
+                nsList.add(packet);
+            }
+        }
+        assertEquals(IpReachabilityMonitor.MIN_NUD_SOLICIT_NUM, nsList.size());
+        for (NeighborSolicitation ns : nsList) {
+            assertUnicastNeighborSolicitation(ns, ROUTER_MAC /* dstMac */,
+                    ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */);
+        }
+        assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */);
+    }
+
+    @Test
+    public void testIpReachabilityMonitor_probeReachable() throws Exception {
+        prepareIpReachabilityMonitorTest();
+
+        NeighborSolicitation ns;
+        while ((ns = getNextNeighborSolicitation()) != null) {
+            // Filter out the NSes used for duplicate address detetction, the target address
+            // is the global IPv6 address inside these NSes.
+            if (ns.nsHdr.target.isLinkLocalAddress()) break;
+        }
+        assertUnicastNeighborSolicitation(ns, ROUTER_MAC /* dstMac */,
+                ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */);
+
+        // Reply Neighbor Advertisement and check notifyLost callback won't be triggered.
+        int flag = NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER | NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
+        final ByteBuffer na = NeighborAdvertisement.build(ROUTER_MAC /* srcMac */,
+                ns.ethHdr.srcMac /* dstMac */, ROUTER_LINK_LOCAL /* srcIp */,
+                ns.ipv6Hdr.srcIp /* dstIp */, flag, ROUTER_LINK_LOCAL /* target */);
+        mPacketReader.sendResponse(na);
+        assertNeverNotifyNeighborLost();
+    }
 }
diff --git a/tests/integration/src/android/net/ip/IpClientRootTest.kt b/tests/integration/src/android/net/ip/IpClientRootTest.kt
index 68d8aab..8a99e4f 100644
--- a/tests/integration/src/android/net/ip/IpClientRootTest.kt
+++ b/tests/integration/src/android/net/ip/IpClientRootTest.kt
@@ -33,6 +33,7 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.net.module.util.DeviceConfigUtils
 import java.lang.System.currentTimeMillis
+import java.net.Inet6Address
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
@@ -45,6 +46,8 @@
 import org.junit.AfterClass
 import org.junit.BeforeClass
 import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.never
 import org.mockito.Mockito.timeout
 import org.mockito.Mockito.verify
 
@@ -260,4 +263,12 @@
         mStore.retrieveNetworkAttributes(l2Key, listener)
         assertNull(listener.getBlockingNetworkAttributes(timeout))
     }
+
+    override fun assertNotifyNeighborLost(targetIp: Inet6Address) {
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onReachabilityLost(anyString())
+    }
+
+    override fun assertNeverNotifyNeighborLost() {
+        verify(mCb, never()).onReachabilityLost(anyString())
+    }
 }
diff --git a/tests/unit/src/android/net/ip/IpClientTest.java b/tests/unit/src/android/net/ip/IpClientTest.java
index c9e486b..d86d0bb 100644
--- a/tests/unit/src/android/net/ip/IpClientTest.java
+++ b/tests/unit/src/android/net/ip/IpClientTest.java
@@ -115,6 +115,7 @@
     private static final String TEST_CLUSTER = "some cluster";
     private static final String TEST_SSID = "test_ssid";
     private static final String TEST_BSSID = "00:11:22:33:44:55";
+    private static final String TEST_BSSID2 = "00:1A:11:22:33:44";
 
     private static final String TEST_GLOBAL_ADDRESS = "1234:4321::548d:2db2:4fcf:ef75/64";
     private static final String[] TEST_LOCAL_ADDRESSES = {
@@ -735,7 +736,18 @@
         final IpClient ipc = makeIpClient(TEST_IFNAME);
         final Layer2Information layer2Info = new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
                 MacAddress.fromString(TEST_BSSID));
-        final MacAddress bssid = ipc.getInitialBssid(layer2Info, null /* ScanReqsultInfo */,
+        final ScanResultInfo scanResultInfo = makeScanResultInfo(TEST_SSID, TEST_BSSID2);
+        final MacAddress bssid = ipc.getInitialBssid(layer2Info, scanResultInfo,
+                true /* isAtLeastS */);
+        assertEquals(bssid, MacAddress.fromString(TEST_BSSID));
+    }
+
+    @Test
+    public void testGetInitialBssidOnSOrAbove_NullScanReqsultInfo() throws Exception {
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        final Layer2Information layer2Info = new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
+                MacAddress.fromString(TEST_BSSID));
+        final MacAddress bssid = ipc.getInitialBssid(layer2Info, null /* ScanResultInfo */,
                 true /* isAtLeastS */);
         assertEquals(bssid, MacAddress.fromString(TEST_BSSID));
     }
@@ -764,7 +776,7 @@
     public void testGetInitialBssidBeforeS() throws Exception {
         final IpClient ipc = makeIpClient(TEST_IFNAME);
         final Layer2Information layer2Info = new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
-                MacAddress.fromString(TEST_BSSID));
+                MacAddress.fromString(TEST_BSSID2));
         final ScanResultInfo scanResultInfo = makeScanResultInfo(TEST_SSID, TEST_BSSID);
         final MacAddress bssid = ipc.getInitialBssid(layer2Info, scanResultInfo,
                 false /* isAtLeastS */);