/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.net.ip;

import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.net.RouteInfo.RTN_UNICAST;
import static android.net.dhcp.DhcpClient.EXPIRED_LEASE;
import static android.net.dhcp.DhcpPacket.CONFIG_MINIMUM_LEASE;
import static android.net.dhcp.DhcpPacket.DHCP_BOOTREQUEST;
import static android.net.dhcp.DhcpPacket.DHCP_CLIENT;
import static android.net.dhcp.DhcpPacket.DHCP_IPV6_ONLY_PREFERRED;
import static android.net.dhcp.DhcpPacket.DHCP_MAGIC_COOKIE;
import static android.net.dhcp.DhcpPacket.DHCP_SERVER;
import static android.net.dhcp.DhcpPacket.ENCAP_L2;
import static android.net.dhcp.DhcpPacket.INADDR_BROADCAST;
import static android.net.dhcp.DhcpPacket.INFINITE_LEASE;
import static android.net.dhcp.DhcpPacket.MIN_V6ONLY_WAIT_MS;
import static android.net.ip.IIpClientCallbacks.DTIM_MULTIPLIER_RESET;
import static android.net.ip.IpClient.CONFIG_IPV6_AUTOCONF_TIMEOUT;
import static android.net.ip.IpClient.CONFIG_ACCEPT_RA_MIN_LFT;
import static android.net.ip.IpClient.CONFIG_APF_COUNTER_POLLING_INTERVAL_SECS;
import static android.net.ip.IpClient.CONFIG_NUD_FAILURE_COUNT_DAILY_THRESHOLD;
import static android.net.ip.IpClient.CONFIG_NUD_FAILURE_COUNT_WEEKLY_THRESHOLD;
import static android.net.ip.IpClient.DEFAULT_APF_COUNTER_POLLING_INTERVAL_SECS;
import static android.net.ip.IpClient.DEFAULT_NUD_FAILURE_COUNT_DAILY_THRESHOLD;
import static android.net.ip.IpClient.DEFAULT_NUD_FAILURE_COUNT_WEEKLY_THRESHOLD;
import static android.net.ip.IpClient.NETWORK_EVENT_NUD_FAILURE_TYPES;
import static android.net.ip.IpClient.ONE_DAY_IN_MS;
import static android.net.ip.IpClient.ONE_WEEK_IN_MS;
import static android.net.ip.IpClient.SIX_HOURS_IN_MS;
import static android.net.ip.IpClientLinkObserver.CLAT_PREFIX;
import static android.net.ip.IpClientLinkObserver.CONFIG_SOCKET_RECV_BUFSIZE;
import static android.net.ip.IpReachabilityMonitor.NUD_MCAST_RESOLICIT_NUM;
import static android.net.ip.IpReachabilityMonitor.nudEventTypeToInt;
import static android.net.ipmemorystore.Status.SUCCESS;
import static android.system.OsConstants.ETH_P_IPV6;
import static android.system.OsConstants.IFA_F_TEMPORARY;
import static android.system.OsConstants.IPPROTO_ICMPV6;
import static android.system.OsConstants.IPPROTO_IPV6;
import static android.system.OsConstants.IPPROTO_UDP;

import static com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress;
import static com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address;
import static com.android.net.module.util.NetworkStackConstants.ALL_DHCP_RELAY_AGENTS_AND_SERVERS;
import static com.android.net.module.util.NetworkStackConstants.ARP_REPLY;
import static com.android.net.module.util.NetworkStackConstants.ARP_REQUEST;
import static com.android.net.module.util.NetworkStackConstants.DHCP6_CLIENT_PORT;
import static com.android.net.module.util.NetworkStackConstants.DHCP6_SERVER_PORT;
import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
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_ADDR_ANY;
import static com.android.net.module.util.NetworkStackConstants.IPV6_PROTOCOL_OFFSET;
import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER;
import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_AUTONOMOUS;
import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_ON_LINK;
import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_DHCPV6_PD_PREFERRED_FLAG_VERSION;
import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION;
import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION;
import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_ROUTER_MAC_CHANGE_FAILURE_ONLY_AFTER_ROAM_VERSION;
import static com.android.testutils.MiscAsserts.assertThrows;
import static com.android.testutils.ParcelUtils.parcelingRoundTrip;
import static com.android.testutils.TestPermissionUtil.runAsShell;

import static junit.framework.Assert.fail;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.longThat;
import static org.mockito.Mockito.after;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.argThat;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.AlarmManager;
import android.app.AlarmManager.OnAlarmListener;
import android.app.Instrumentation;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.net.DhcpResultsParcelable;
import android.net.IIpMemoryStore;
import android.net.INetd;
import android.net.InetAddresses;
import android.net.InterfaceConfigurationParcel;
import android.net.IpPrefix;
import android.net.Layer2InformationParcelable;
import android.net.Layer2PacketParcelable;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.MacAddress;
import android.net.Network;
import android.net.NetworkAgentConfig;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.NetworkSpecifier;
import android.net.NetworkStackIpMemoryStore;
import android.net.RouteInfo;
import android.net.TestNetworkInterface;
import android.net.TestNetworkManager;
import android.net.Uri;
import android.net.dhcp.DhcpClient;
import android.net.dhcp.DhcpDeclinePacket;
import android.net.dhcp.DhcpDiscoverPacket;
import android.net.dhcp.DhcpPacket;
import android.net.dhcp.DhcpPacket.ParseException;
import android.net.dhcp.DhcpRequestPacket;
import android.net.dhcp6.Dhcp6AddrRegTracker;
import android.net.dhcp6.Dhcp6Client;
import android.net.dhcp6.Dhcp6PacketDispatcher;
import android.net.ipmemorystore.NetworkAttributes;
import android.net.ipmemorystore.OnNetworkAttributesRetrievedListener;
import android.net.ipmemorystore.OnNetworkEventCountRetrievedListener;
import android.net.ipmemorystore.OnStatusListener;
import android.net.ipmemorystore.Status;
import android.net.networkstack.TestNetworkStackServiceClient;
import android.net.networkstack.aidl.dhcp.DhcpOption;
import android.net.networkstack.aidl.ip.ReachabilityLossInfoParcelable;
import android.net.networkstack.aidl.ip.ReachabilityLossReason;
import android.net.shared.Layer2Information;
import android.net.shared.ProvisioningConfiguration;
import android.net.shared.ProvisioningConfiguration.ScanResultInfo;
import android.net.util.HostnameTransliterator;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.SystemClock;
import android.platform.test.annotations.EnableFlags;
import android.provider.Settings;
import android.stats.connectivity.NudEventType;
import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;
import android.util.Pair;

import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;

import com.android.internal.util.HexDump;
import com.android.internal.util.StateMachine;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.ArrayTrackRecord;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.Ipv6Utils;
import com.android.net.module.util.PacketBuilder;
import com.android.net.module.util.SharedLog;
import com.android.net.module.util.Struct;
import com.android.net.module.util.arp.ArpPacket;
import com.android.net.module.util.dhcp6.Dhcp6Packet;
import com.android.net.module.util.dhcp6.Dhcp6Packet.PrefixDelegation;
import com.android.net.module.util.dhcp6.Dhcp6RebindPacket;
import com.android.net.module.util.dhcp6.Dhcp6RenewPacket;
import com.android.net.module.util.dhcp6.Dhcp6RequestPacket;
import com.android.net.module.util.dhcp6.Dhcp6SolicitPacket;
import com.android.net.module.util.ip.IpNeighborMonitor;
import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEventConsumer;
import com.android.net.module.util.netlink.NetlinkUtils;
import com.android.net.module.util.netlink.StructNdOptPref64;
import com.android.net.module.util.structs.EthernetHeader;
import com.android.net.module.util.structs.IaPrefixOption;
import com.android.net.module.util.structs.Ipv6Header;
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.R;
import com.android.networkstack.apishim.CaptivePortalDataShimImpl;
import com.android.networkstack.ipmemorystore.IpMemoryStoreService;
import com.android.networkstack.mainline.beta.Flags;
import com.android.networkstack.metrics.IpProvisioningMetrics;
import com.android.networkstack.metrics.IpReachabilityMonitorMetrics;
import com.android.networkstack.metrics.NetworkQuirkMetrics;
import com.android.networkstack.packets.NeighborAdvertisement;
import com.android.networkstack.packets.NeighborSolicitation;
import com.android.networkstack.util.NetworkStackUtils;
import com.android.server.NetworkStackService.NetworkStackServiceManager;
import com.android.testutils.CompatUtil;
import com.android.testutils.ConnectivityDiagnosticsCollector;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.HandlerUtils;
import com.android.testutils.PollPacketReader;
import com.android.testutils.TestableNetworkAgent;
import com.android.testutils.TestableNetworkCallback;

import kotlin.Lazy;
import kotlin.LazyKt;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileReader;
import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;

/**
 * Base class for IpClient tests.
 *
 * Tests in this class can either be run with signature permissions, or with root access.
 */
@SmallTest
public abstract class IpClientIntegrationTestCommon {
    private static final String TAG = IpClientIntegrationTestCommon.class.getSimpleName();
    private static final int DATA_BUFFER_LEN = 4096;
    private static final int PACKET_TIMEOUT_MS = 5_000;
    protected static final String TEST_CLUSTER = "some cluster";
    private static final int TEST_LEASE_DURATION_S = 3_600; // 1 hour
    private static final int TEST_IPV6_ONLY_WAIT_S = 1_800; // 30 min
    private static final int TEST_LOWER_IPV6_ONLY_WAIT_S = (int) (MIN_V6ONLY_WAIT_MS / 1000 - 1);
    private static final int TEST_ZERO_IPV6_ONLY_WAIT_S = 0;
    private static final long TEST_MAX_IPV6_ONLY_WAIT_S = 0xffffffffL;
    private static final int TEST_DEVICE_OWNER_APP_UID = 14242;
    private static final String TEST_DEVICE_OWNER_APP_PACKAGE = "com.example.deviceowner";
    protected static final String TEST_L2KEY = "some l2key";

    // TODO: move to NetlinkConstants, NetworkStackConstants, or OsConstants.
    private static final int IFA_F_STABLE_PRIVACY = 0x800;
    // To fix below AndroidLint warning:
    // [InlinedApi] Field requires version 3 of the U Extensions SDK (current min is 0).
    private static final int RTN_UNREACHABLE =
            SdkLevel.isAtLeastT() ? RouteInfo.RTN_UNREACHABLE : 7;

    protected static final long TEST_TIMEOUT_MS = 2_000L;
    private static final long TEST_WAIT_ENOBUFS_TIMEOUT_MS = 30_000L;
    private static final long TEST_WAIT_RENEW_REBIND_RETRANSMIT_MS = 15_000L;
    // To prevent the flakiness about deprecationTime and expirationTime check, +/- 4s tolerance
    // should be enough between the timestamp when the IP provisioning completes successfully and
    // when IpClientLinkObserver sees the RTM_NEWADDR netlink events.
    private static final long TEST_LIFETIME_TOLERANCE_MS = 4_000L;
    private static final long TEST_POLL_NEIGHBOR_PARAMETER_MS = 500L;
    private static final int TEST_ARP_LOCKTIME_MS = 1500;
    private static final int TEST_DELAY_FIRST_PROBE_TIME_S = 2;

    private static final byte TEST_PIO_FLAGS_P_UNSET = (byte) 0xC0; // L=1,A=1,R=0,P=0
    private static final byte TEST_PIO_FLAGS_P_SET = (byte) 0xD0;   // L=1,A=1,R=0,P=1

    // Note that this test contains @EnableFlags annotations but no SetFlagsRule. This is because
    // this test is run with both an in-process (IpClientSignatureTest) and
    // out-of-process (IpClientRootTest) IpClient instance. Only the in-process test is able to
    // properly set the flags, so the SetFlagsRule is part of the IpClientSignatureTest subclass.
    // The IpClientRootTest uses a custom rule that optionally skips tests based on the @EnableFlags
    // values without actually enabling them (similar to the existing CheckFlagsRule).
    @Rule
    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
    @Rule
    public final TestName mTestNameRule = new TestName();

    /**
     * Indicates that a test requires signature permissions to run.
     *
     * Such tests can only be run on devices that use known signing keys, so this annotation must be
     * avoided as much as possible. Consider whether the test can be written to use shell and root
     * shell permissions, and run against the NetworkStack AIDL interface (IIpClient) instead.
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD})
    private @interface SignatureRequiredTest {
        String reason();
    }

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD})
    @Repeatable(FlagArray.class)
    @interface Flag {
        String name();
        boolean enabled();
    }

    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @interface FlagArray {
        Flag[] value();
    }

    /**** BEGIN signature required test members ****/
    // Do not use unless the test *really* cannot be written to exercise IIpClient without mocks.
    // Tests using the below members must be annotated with @SignatureRequiredTest (otherwise the
    // members will be null), and can only be run on devices that use known signing keys.
    // The members could technically be moved to the IpClientIntegrationTest subclass together with
    // the tests requiring signature permissions, but this would make it harder to follow tests in
    // multiple classes, and harder to migrate tests between signature required and not required.

    @Mock private Context mContext;
    @Mock private ConnectivityManager mCm;
    @Mock private Resources mResources;
    @Mock private AlarmManager mAlarm;
    @Mock private ContentResolver mContentResolver;
    @Mock private NetworkStackServiceManager mNetworkStackServiceManager;
    @Mock private IpMemoryStoreService mIpMemoryStoreService;
    @Mock private PowerManager.WakeLock mTimeoutWakeLock;
    @Mock protected NetworkStackIpMemoryStore mIpMemoryStore;
    @Mock private NetworkQuirkMetrics.Dependencies mNetworkQuirkMetricsDeps;
    @Mock private IpReachabilityMonitorMetrics mIpReachabilityMonitorMetrics;
    @Mock private DevicePolicyManager mDevicePolicyManager;
    @Mock private PackageManager mPackageManager;
    @Mock private Dhcp6AddrRegTracker mDhcp6AddrRegTracker;
    @Spy private INetd mNetd;

    protected IpClient mIpc;
    protected Dependencies mDependencies;
    protected List<Pair<String, Pair<Long, Integer>>> mNetworkEvents = new ArrayList<>();

    /***** END signature required test members *****/

    protected IIpClientCallbacks mCb;
    private IIpClient mIIpClient;
    private String mIfaceName;
    private HandlerThread mPacketReaderThread;
    private Handler mHandler;
    private PollPacketReader mPacketReader;
    private FileDescriptor mTapFd;
    private byte[] mClientMac;
    private InetAddress mClientIpAddress;
    private TestableNetworkAgent mNetworkAgent;
    private HandlerThread mNetworkAgentThread;

    private boolean mIsSignatureRequiredTest;

    // ReadHeads for various packet streams. Cannot be initialized in @Before because ReadHead is
    // single-thread-only, and AndroidJUnitRunner runs @Before and @Test on different threads.
    // While it looks like these are created only once per test, they are actually created once per
    // test method because JUnit recreates a fresh test class instance before every test method.
    private Lazy<ArrayTrackRecord<byte[]>.ReadHead> mDhcpPacketReadHead =
            LazyKt.lazy(() -> mPacketReader.getReceivedPackets().newReadHead());
    private Lazy<ArrayTrackRecord<byte[]>.ReadHead> mArpPacketReadHead =
            LazyKt.lazy(() -> mPacketReader.getReceivedPackets().newReadHead());
    private Lazy<ArrayTrackRecord<byte[]>.ReadHead> mDhcp6PacketReadHead =
            LazyKt.lazy(() -> mPacketReader.getReceivedPackets().newReadHead());

    // Ethernet header
    private static final int ETH_HEADER_LEN = 14;

    // IP header
    private static final int IPV4_HEADER_LEN = 20;
    private static final int IPV6_HEADER_LEN = 40;
    private static final int IPV4_SRC_ADDR_OFFSET = ETH_HEADER_LEN + 12;
    private static final int IPV4_DST_ADDR_OFFSET = IPV4_SRC_ADDR_OFFSET + 4;

    // UDP header
    private static final int UDP_HEADER_LEN = 8;
    private static final int UDP_HEADER_OFFSET = ETH_HEADER_LEN + IPV4_HEADER_LEN;
    private static final int UDP_SRC_PORT_OFFSET = UDP_HEADER_OFFSET + 0;

    // DHCP header
    private static final int DHCP_HEADER_OFFSET = ETH_HEADER_LEN + IPV4_HEADER_LEN
            + UDP_HEADER_LEN;
    private static final int DHCP_MESSAGE_OP_CODE_OFFSET = DHCP_HEADER_OFFSET + 0;
    private static final int DHCP_OPTION_MAGIC_COOKIE_OFFSET = DHCP_HEADER_OFFSET + 236;

    // DHCPv6 header
    private static final int DHCP6_HEADER_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN
            + UDP_HEADER_LEN;

    private static final Inet4Address SERVER_ADDR = ipv4Addr("192.168.1.100");
    private static final Inet4Address CLIENT_ADDR = ipv4Addr("192.168.1.2");
    private static final Inet4Address CLIENT_ADDR_NEW = ipv4Addr("192.168.1.3");
    private static final Inet4Address INADDR_ANY = ipv4Addr("0.0.0.0");
    private static final int PREFIX_LENGTH = 24;
    private static final Inet4Address NETMASK = getPrefixMaskAsInet4Address(PREFIX_LENGTH);
    private static final Inet4Address BROADCAST_ADDR = getBroadcastAddress(
            SERVER_ADDR, PREFIX_LENGTH);
    private static final String IPV6_LINK_LOCAL_PREFIX = "fe80::/64";
    private static final String IPV4_TEST_SUBNET_PREFIX = "192.168.1.0/24";
    private static final String IPV4_ANY_ADDRESS_PREFIX = "0.0.0.0/0";
    private static final String HOSTNAME = "testhostname";
    private static final String TEST_IPV6_PREFIX = "2001:db8:1::/64";
    private static final String TEST_IPV6_ULA_PREFIX = "fd00:1234:5678:9abc::/64";
    private static final String IPV6_OFF_LINK_DNS_SERVER = "2001:4860:4860::64";
    private static final String TEST_DHCP6_DELEGATED_PREFIX = "2001:db8:dead:beef::/64";
    private static final String IPV6_ON_LINK_DNS_SERVER = "2001:db8:1::64";
    private static final int TEST_DEFAULT_MTU = 1500;
    private static final int TEST_MIN_MTU = 1280;
    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 MacAddress ON_LINK_DNS_SERVER_MAC =
            MacAddress.fromString("00:1A:11:AA:BB:CC");
    private static final Inet6Address ROUTER_LINK_LOCAL = ipv6Addr("fe80::1");
    private static final byte[] ROUTER_DUID = new byte[] {
            // type: Link-layer address, hardware type: EUI64(27)
            (byte) 0x00, (byte) 0x03, (byte) 0x00, (byte) 0x1b,
            // set 7th bit, and copy the first 3 bytes of mac address
            (byte) 0x02, (byte) 0x1A, (byte) 0x11,
            (byte) 0xFF, (byte) 0xFE,
            // copy the last 3 bytes of mac address
            (byte) 0x22, (byte) 0x33, (byte) 0x44,
    };
    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";
    private static final byte LEGACY_TEST_VENDOR_SPECIFIC_IE_TYPE = 0x11;
    private static final byte TEST_VENDOR_SPECIFIC_IE_TYPE = 0x21;
    private static final int TEST_VENDOR_SPECIFIC_IE_ID = 0xdd;

    private static final String TEST_DEFAULT_SSID = "test_ssid";
    private static final String TEST_DEFAULT_BSSID = "00:11:22:33:44:55";
    private static final String TEST_DHCP_ROAM_BSSID = "00:4e:35:17:98:55";
    private static final String TEST_DHCP_ROAM_L2KEY = "roaming_l2key";
    private static final String TEST_DHCP_ROAM_CLUSTER = "roaming_cluster";
    private static final byte[] TEST_AP_OUI = new byte[] { 0x00, 0x1A, 0x11 };
    private static final byte[] TEST_OEM_OUI = new byte[] {(byte) 0x00, (byte) 0x17, (byte) 0xc3};
    private static final String TEST_OEM_VENDOR_ID = "vendor-class-identifier";
    private static final byte[] TEST_OEM_USER_CLASS_INFO = new byte[] {
            // Instance of User Class: [0]
            (byte) 0x03, /* UC_Len_0 */ (byte) 0x11, (byte) 0x22, (byte) 0x33,
            // Instance of User Class: [1]
            (byte) 0x03, /* UC_Len_1 */ (byte) 0x44, (byte) 0x55, (byte) 0x66,
    };

    protected class Dependencies extends IpClient.Dependencies {
        private DhcpClient mDhcpClient;
        private Dhcp6Client mDhcp6Client;
        private boolean mIsHostnameConfigurationEnabled;
        private String mHostname;
        private boolean mIsInterfaceRecovered;

        public void setHostnameConfiguration(final boolean enable, final String hostname) {
            mIsHostnameConfigurationEnabled = enable;
            mHostname = hostname;
        }

        // Enable this flag to simulate the interface has been added back after removing
        // on the provisioning start. However, the actual tap interface has been removed,
        // interface parameters query will get null when attempting to restore Interface
        // MTU. Create a new InterfaceParams instance and return instead just for interface
        // toggling test case.
        public void simulateInterfaceRecover() {
            mIsInterfaceRecovered = true;
        }

        @Override
        public InterfaceParams getInterfaceParams(String ifname) {
            return mIsInterfaceRecovered
                    ? new InterfaceParams(ifname, 1 /* index */,
                            MacAddress.fromString("00:11:22:33:44:55"))
                    : super.getInterfaceParams(ifname);
        }

        @Override
        public INetd getNetd(Context context) {
            return mNetd;
        }

        @Override
        public NetworkStackIpMemoryStore getIpMemoryStore(Context context,
                NetworkStackServiceManager nssManager) {
            return mIpMemoryStore;
        }

        @Override
        public DhcpClient makeDhcpClient(Context context, StateMachine controller,
                InterfaceParams ifParams, DhcpClient.Dependencies deps) {
            mDhcpClient = DhcpClient.makeDhcpClient(context, controller, ifParams, deps);
            return mDhcpClient;
        }

        @Override
        public Dhcp6Client makeDhcp6Client(Context context, StateMachine controller,
                InterfaceParams ifParams, Dhcp6PacketDispatcher dispatcher,
                Dhcp6Client.Dependencies deps) {
            mDhcp6Client =
                    Dhcp6Client.makeDhcp6Client(context, controller, ifParams, dispatcher, deps);
            return mDhcp6Client;
        }

        @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, callback,
                    usingMultinetworkPolicyTracker, deps, netd);
        }

        @Override
        public boolean isFeatureEnabled(final Context context, final String name) {
            return IpClientIntegrationTestCommon.this.isFeatureEnabled(name);
        }

        @Override
        public boolean isFeatureNotChickenedOut(final Context context, final String name) {
            return IpClientIntegrationTestCommon.this.isFeatureNotChickenedOut(name);
        }

        @Override
        public int getDeviceConfigPropertyInt(String name, int ignoredDefaultValue) {
            // Default is never used because all device config properties must be mocked by test.
            try {
                return Integer.parseInt(getDeviceConfigProperty(name));
            } catch (NumberFormatException e) {
                throw new IllegalStateException("Non-mocked device config property " + name);
            }
        }

        @Override
        public Dhcp6Client.Dependencies getDhcp6ClientDependencies() {
            return new Dhcp6Client.Dependencies() {
                @Override
                public int getDeviceConfigPropertyInt(String name, int defaultValue) {
                    return Dependencies.this.getDeviceConfigPropertyInt(name,
                            defaultValue);
                }
            };
        }

        @Override
        public DhcpClient.Dependencies getDhcpClientDependencies(
                NetworkStackIpMemoryStore ipMemoryStore, IpProvisioningMetrics metrics) {
            return new DhcpClient.Dependencies(ipMemoryStore, metrics) {
                @Override
                public boolean isFeatureEnabled(final Context context, final String name) {
                    return Dependencies.this.isFeatureEnabled(context, name);
                }

                @Override
                public boolean isFeatureNotChickenedOut(final Context context, final String name) {
                    return Dependencies.this.isFeatureNotChickenedOut(context, name);
                }

                @Override
                public int getIntDeviceConfig(final String name, int minimumValue,
                        int maximumValue, int defaultValue) {
                    return Dependencies.this.getDeviceConfigPropertyInt(name, defaultValue);
                }

                @Override
                public int getIntDeviceConfig(final String name, int defaultValue) {
                    return Dependencies.this.getDeviceConfigPropertyInt(name, defaultValue);
                }

                @Override
                public PowerManager.WakeLock getWakeLock(final PowerManager powerManager) {
                    return mTimeoutWakeLock;
                }

                @Override
                public boolean getSendHostnameOverlaySetting(final Context context) {
                    return mIsHostnameConfigurationEnabled;
                }

                @Override
                public String getDeviceName(final Context context) {
                    return mHostname;
                }
            };
        }

        @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);
                }

                public boolean isFeatureEnabled(final Context context, final String name) {
                    return Dependencies.this.isFeatureEnabled(context, name);
                }

                public boolean isFeatureNotChickenedOut(final Context context, final String name) {
                    return Dependencies.this.isFeatureNotChickenedOut(context, name);
                }

                public IpReachabilityMonitorMetrics getIpReachabilityMonitorMetrics() {
                    return mIpReachabilityMonitorMetrics;
                }
            };
        }

        @Override
        public NetworkQuirkMetrics getNetworkQuirkMetrics() {
            return new NetworkQuirkMetrics(mNetworkQuirkMetricsDeps);
        }

        @Override
        public Dhcp6AddrRegTracker makeDhcp6AddrRegTracker(Context context, Handler handler,
                String ifName, Dhcp6PacketDispatcher dispatcher) {
            return mDhcp6AddrRegTracker;
        }
    }

    @NonNull
    protected abstract IIpClient makeIIpClient(
            @NonNull String ifaceName, @NonNull IIpClientCallbacks cb);

    // In production. features are enabled if the flag is lower than the package version.
    // For testing, we can just use 1 for enabled and -1 for disabled or chickened out.
    static final String FEATURE_ENABLED = "1";
    static final String FEATURE_DISABLED = "-1";

    final void setFeatureEnabled(String feature, boolean enabled) {
        setDeviceConfigProperty(feature, enabled ? FEATURE_ENABLED : FEATURE_DISABLED);
    }

    final void setFeatureChickenedOut(String feature, boolean chickenedOut) {
        setDeviceConfigProperty(feature, chickenedOut ? FEATURE_DISABLED : FEATURE_ENABLED);
    }

    final void setDeviceConfigProperty(String name, int value) {
        setDeviceConfigProperty(name, Integer.toString(value));
    }

    protected abstract void setDeviceConfigProperty(String name, String value);

    protected abstract String getDeviceConfigProperty(String name);

    protected abstract boolean isFeatureEnabled(String name);

    protected abstract boolean isFeatureNotChickenedOut(String name);

    protected abstract boolean useNetworkStackSignature();

    protected abstract NetworkAttributes getStoredNetworkAttributes(String l2Key, long timeout);

    protected abstract void storeNetworkAttributes(String l2Key, NetworkAttributes na);

    protected abstract void assertIpMemoryNeverStoreNetworkAttributes(String l2Key, long timeout);

    protected abstract int readNudSolicitNumInSteadyStateFromResource();

    protected abstract int readNudSolicitNumPostRoamingFromResource();

    protected abstract void storeNetworkEvent(String cluster, long now, long expiry, int eventType);

    protected abstract int[] getStoredNetworkEventCount(String cluster, long[] sinceTimes,
            int[] eventType, long timeout);

    protected final boolean testSkipped() {
        if (!useNetworkStackSignature() && !TestNetworkStackServiceClient.isSupported()) {
            fail("Device running root tests doesn't support TestNetworkStackServiceClient.");
        }
        return !useNetworkStackSignature() && mIsSignatureRequiredTest;
    }

    private static InetAddress ipAddr(String addr) {
        return InetAddresses.parseNumericAddress(addr);
    }

    private static Inet4Address ipv4Addr(String addr) {
        return (Inet4Address) ipAddr(addr);
    }

    private static Inet6Address ipv6Addr(String addr) {
        return (Inet6Address) ipAddr(addr);
    }

    private void setDhcpFeatures(final boolean isRapidCommitEnabled,
            final boolean isDhcpIpConflictDetectEnabled) {
        setFeatureEnabled(NetworkStackUtils.DHCP_RAPID_COMMIT_VERSION, isRapidCommitEnabled);
        setFeatureEnabled(NetworkStackUtils.DHCP_IP_CONFLICT_DETECT_VERSION,
                isDhcpIpConflictDetectEnabled);
    }

    private void setDeviceConfigForMaxDtimMultiplier() {
        setDeviceConfigProperty(IpClient.CONFIG_INITIAL_PROVISIONING_DTIM_DELAY_MS,
                500 /* default value */);
        setDeviceConfigProperty(IpClient.CONFIG_MULTICAST_LOCK_MAX_DTIM_MULTIPLIER,
                IpClient.DEFAULT_MULTICAST_LOCK_MAX_DTIM_MULTIPLIER);
        setDeviceConfigProperty(IpClient.CONFIG_IPV6_ONLY_NETWORK_MAX_DTIM_MULTIPLIER,
                IpClient.DEFAULT_IPV6_ONLY_NETWORK_MAX_DTIM_MULTIPLIER);
        setDeviceConfigProperty(IpClient.CONFIG_IPV4_ONLY_NETWORK_MAX_DTIM_MULTIPLIER,
                IpClient.DEFAULT_IPV4_ONLY_NETWORK_MAX_DTIM_MULTIPLIER);
        setDeviceConfigProperty(IpClient.CONFIG_DUAL_STACK_MAX_DTIM_MULTIPLIER,
                IpClient.DEFAULT_DUAL_STACK_MAX_DTIM_MULTIPLIER);
        setDeviceConfigProperty(IpClient.CONFIG_BEFORE_IPV6_PROV_MAX_DTIM_MULTIPLIER,
                IpClient.DEFAULT_BEFORE_IPV6_PROV_MAX_DTIM_MULTIPLIER);
    }

    @Before
    public void setUp() throws Exception {
        final String testMethodName = mTestNameRule.getMethodName();
        final Method testMethod = IpClientIntegrationTestCommon.class.getMethod(testMethodName);
        mIsSignatureRequiredTest = testMethod.getAnnotation(SignatureRequiredTest.class) != null;
        assumeFalse(testSkipped());

        // Enable replacement of netd usage with netlink in IpClient.
        setFeatureEnabled(NetworkStackUtils.IPCLIENT_REPLACE_NETD_WITH_NETLINK_VERSION,
                true /* isIpClientReplaceNetdWithNetlinkEnabled */);

        // Set flags based on test method annotations.
        final Flag[] flags = testMethod.getAnnotationsByType(Flag.class);
        for (Flag flag : flags) {
            setFeatureEnabled(flag.name(), flag.enabled());
        }

        setUpTapInterface();
        // It turns out that Router Solicitation will also be sent out even after the tap interface
        // is brought up, however, we want to wait for RS which is sent due to IPv6 stack is enabled
        // in the test code. The early RS might bring kind of race, for example, the IPv6 stack has
        // not been enabled when test code sees the RS, then kernel will not process RA even if we
        // replies immediately after receiving RS. Always waiting for the first RS show up after
        // interface is brought up helps prevent the race.
        waitForRouterSolicitation();

        mCb = mock(IIpClientCallbacks.class);
        if (useNetworkStackSignature()) {
            setUpMocks();
            setUpIpClient();
            // Enable packet retransmit alarm in DhcpClient.
            enableRealAlarm("DhcpClient." + mIfaceName + ".KICK");
            enableRealAlarm("DhcpClient." + mIfaceName + ".RENEW");
            // Enable alarm for IPv6 autoconf via SLAAC in IpClient.
            enableRealAlarm("IpClient." + mIfaceName + ".EVENT_IPV6_AUTOCONF_TIMEOUT");
            // Enable packet retransmit alarm in Dhcp6Client.
            enableRealAlarm("Dhcp6Client." + mIfaceName + ".KICK");
        }

        mIIpClient = makeIIpClient(mIfaceName, mCb);

        // Enable multicast filtering after creating IpClient instance, make the integration test
        // more realistic.
        mIIpClient.setMulticastFilter(true);
        setDeviceConfigForMaxDtimMultiplier();
        // Set IPv6 autoconf timeout. For signature tests, it has disabled the provisioning delay,
        // use a small timeout value to speed up the test execution; For root tests, we have to
        // wait a bit longer to make sure that we do see the success IPv6 provisioning, otherwise,
        // the global IPv6 address may show up later due to DAD, so we consider that autoconf fails
        // in this case and start DHCPv6 Prefix Delegation then.
        final int timeout = useNetworkStackSignature() ? 500 : (int) TEST_TIMEOUT_MS;
        setDeviceConfigProperty(IpClient.CONFIG_IPV6_AUTOCONF_TIMEOUT, timeout /* default value */);
        // Set DHCP minimum lease.
        setDeviceConfigProperty(DhcpPacket.CONFIG_MINIMUM_LEASE, DhcpPacket.DEFAULT_MINIMUM_LEASE);
    }

    protected void setUpMocks() throws Exception {
        MockitoAnnotations.initMocks(this);

        mDependencies = new Dependencies();
        when(mContext.getSystemService(Context.ALARM_SERVICE)).thenReturn(mAlarm);
        when(mContext.getSystemService(ConnectivityManager.class)).thenReturn(mCm);
        when(mContext.getSystemService(Context.DEVICE_POLICY_SERVICE))
                .thenReturn(mDevicePolicyManager);
        when(mContext.getPackageManager()).thenReturn(mPackageManager);
        when(mContext.getResources()).thenReturn(mResources);
        when(mResources.getInteger(eq(R.integer.config_nud_postroaming_solicit_num))).thenReturn(5);
        when(mResources.getInteger(eq(R.integer.config_nud_postroaming_solicit_interval)))
                 .thenReturn(750);
        when(mResources.getInteger(eq(R.integer.config_nud_steadystate_solicit_num)))
                 .thenReturn(10);
        when(mResources.getInteger(eq(R.integer.config_nud_steadystate_solicit_interval)))
                 .thenReturn(750);
        when(mContext.getContentResolver()).thenReturn(mContentResolver);
        when(mNetworkStackServiceManager.getIpMemoryStoreService())
                .thenReturn(mIpMemoryStoreService);
        when(mCb.getInterfaceVersion()).thenReturn(IpClient.VERSION_ADDED_REACHABILITY_FAILURE);
        // This mock is required, otherwise, ignoreIPv6ProvisioningLoss variable is always true,
        // and IpReachabilityMonitor#avoidingBadLinks() will always return false as well, that
        // results in the target tested IPv6 off-link DNS server won't be removed from LP and
        // notifyLost won't be invoked, or the wrong code path when receiving RA with 0 router
        // liftime.
        when(mCm.shouldAvoidBadWifi()).thenReturn(true);

        when(mDevicePolicyManager.getDeviceOwnerComponentOnAnyUser()).thenReturn(
                new ComponentName(TEST_DEVICE_OWNER_APP_PACKAGE, "com.example.SomeClass"));
        when(mPackageManager.getPackagesForUid(TEST_DEVICE_OWNER_APP_UID)).thenReturn(
                new String[] { TEST_DEVICE_OWNER_APP_PACKAGE });

        // Store a network event to db.
        doAnswer(invocation -> {
            final String cluster = invocation.getArgument(0);
            final long timestamp = invocation.getArgument(1);
            final long expiry = invocation.getArgument(2);
            final int eventType = invocation.getArgument(3);
            storeNetworkEvent(cluster, timestamp, expiry, eventType);
            ((OnStatusListener) invocation.getArgument(4)).onComplete(new Status(SUCCESS));
            return null;
        }).when(mIpMemoryStore).storeNetworkEvent(eq(TEST_CLUSTER), anyLong(), anyLong(), anyInt(),
                any());

        // Retrieve the network event count from db.
        doAnswer(invocation -> {
            final String cluster = invocation.getArgument(0);
            final long[] sinceTimes = invocation.getArgument(1);
            final int[] eventType = invocation.getArgument(2);
            ((OnNetworkEventCountRetrievedListener) invocation.getArgument(3))
                    .onNetworkEventCountRetrieved(
                            new Status(SUCCESS),
                            getStoredNetworkEventCount(cluster, sinceTimes, eventType,
                                    0 /* timeout not used */));
            return null;
        }).when(mIpMemoryStore).retrieveNetworkEventCount(eq(TEST_CLUSTER), any(), any(), any());

        setDeviceConfigProperty(DhcpClient.DHCP_RESTART_CONFIG_DELAY, 10);
        setDeviceConfigProperty(DhcpClient.ARP_FIRST_PROBE_DELAY_MS, 10);
        setDeviceConfigProperty(DhcpClient.ARP_PROBE_MIN_MS, 10);
        setDeviceConfigProperty(DhcpClient.ARP_PROBE_MAX_MS, 20);
        setDeviceConfigProperty(DhcpClient.ARP_FIRST_ANNOUNCE_DELAY_MS, 10);
        setDeviceConfigProperty(DhcpClient.ARP_ANNOUNCE_INTERVAL_MS, 10);

        // Set the initial netlink socket receive buffer size to a minimum of 100KB to ensure test
        // cases are still working, meanwhile in order to easily overflow the receive buffer by
        // sending as few RAs as possible for test case where it's used to verify ENOBUFS.
        setDeviceConfigProperty(CONFIG_SOCKET_RECV_BUFSIZE, 100 * 1024);

        // Set the timeout to wait IPv6 autoconf to complete.
        setDeviceConfigProperty(CONFIG_IPV6_AUTOCONF_TIMEOUT, 500);

        // Set the minimal RA lifetime value, any RA section with liftime below this value will be
        // ignored.
        setDeviceConfigProperty(CONFIG_ACCEPT_RA_MIN_LFT, 67);

        // Set the polling interval to update APF data snapshot.
        setDeviceConfigProperty(CONFIG_APF_COUNTER_POLLING_INTERVAL_SECS,
                DEFAULT_APF_COUNTER_POLLING_INTERVAL_SECS);

        // Set the NUD failure event count daily and weekly thresholds.
        setDeviceConfigProperty(CONFIG_NUD_FAILURE_COUNT_DAILY_THRESHOLD,
                DEFAULT_NUD_FAILURE_COUNT_DAILY_THRESHOLD);
        setDeviceConfigProperty(CONFIG_NUD_FAILURE_COUNT_WEEKLY_THRESHOLD,
                DEFAULT_NUD_FAILURE_COUNT_WEEKLY_THRESHOLD);
    }

    private void awaitIpClientShutdown() throws Exception {
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onQuit();
    }

    @After
    public void tearDown() throws Exception {
        if (testSkipped()) return;
        if (mNetworkAgent != null) {
            mNetworkAgent.unregister();
        }
        if (mNetworkAgentThread != null) {
            mNetworkAgentThread.quitSafely();
            mNetworkAgentThread.join();
        }
        teardownTapInterface();
        mIIpClient.shutdown();
        awaitIpClientShutdown();
    }

    private void setUpTapInterface() throws Exception {
        final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
        final TestNetworkInterface iface = runAsShell(MANAGE_TEST_NETWORKS, () -> {
            final TestNetworkManager tnm =
                    inst.getContext().getSystemService(TestNetworkManager.class);
            try {
                return tnm.createTapInterface(true /* carrierUp */, true /* bringUp */,
                        true /* disableIpv6ProvisioningDelay */);
            } catch (NoSuchMethodError e) {
                // createTapInterface(boolean, boolean, boolean) has been introduced since T,
                // use the legancy API if the method is not found on previous platforms.
                return tnm.createTapInterface();
            }
        });
        mIfaceName = iface.getInterfaceName();
        mClientMac = getIfaceMacAddr(mIfaceName).toByteArray();
        mPacketReaderThread = new HandlerThread(
                IpClientIntegrationTestCommon.class.getSimpleName());
        mPacketReaderThread.start();
        mHandler = mPacketReaderThread.getThreadHandler();

        // Detach the FileDescriptor from the ParcelFileDescriptor.
        // Otherwise, the garbage collector might call the ParcelFileDescriptor's finalizer, which
        // closes the FileDescriptor and destroys our tap interface. An alternative would be to
        // make the ParcelFileDescriptor or the TestNetworkInterface a class member so they never
        // go out of scope.
        mTapFd = new FileDescriptor();
        mTapFd.setInt$(iface.getFileDescriptor().detachFd());
        mPacketReader = new PollPacketReader(mHandler, mTapFd, DATA_BUFFER_LEN);
        mHandler.post(() -> mPacketReader.start());
    }

    private TestNetworkInterface setUpClatInterface(@NonNull String baseIface) throws Exception {
        final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
        final TestNetworkInterface iface = runAsShell(MANAGE_TEST_NETWORKS, () -> {
            final TestNetworkManager tnm =
                    inst.getContext().getSystemService(TestNetworkManager.class);
            return tnm.createTapInterface(false /* bringUp */, CLAT_PREFIX + baseIface);
        });
        return iface;
    }

    private void teardownTapInterface() throws Exception {
        if (mPacketReader != null) {
            mHandler.post(() -> mPacketReader.stop());  // Also closes the socket
            mTapFd = null;
        }
        if (mPacketReaderThread != null) {
            mPacketReaderThread.quitSafely();
            mPacketReaderThread.join();
        }
    }

    /**
     * Set the sysctl "delay_first_probe_time" by executing a shell command as root.
     *
     * Directly using "su root echo delay > /proc/sys/net/ipv4/neigh/delay_first_probe_time" fails
     * because only the "echo delay" command runs as root; the redirection ">" and the following
     * sysctl path string is not performed by the root shell, resulting in permission errors.
     *
     * Theoretically we want to run "su root sh -c 'echo delay > /proc/sys/net/ipv4(v6)/neigh/
     * delay_first_probe_time'" to change the sysctl, however, the `executeShellCommand`
     * function splits commands based on spaces, even within single quotes, so this doesn't work.
     * Instead, use `executeShellCommandRw` to execute a shell command receiving from stdin.
     */
    private void setNudDelayFirstProbeTime(int delay, String family) throws Exception {
        final ParcelFileDescriptor[] fds = InstrumentationRegistry.getInstrumentation()
                .getUiAutomation().executeShellCommandRw("su root sh");

        final ParcelFileDescriptor stdout = fds[0];
        final ParcelFileDescriptor stdin = fds[1];
        try (ParcelFileDescriptor.AutoCloseOutputStream output =
                     new ParcelFileDescriptor.AutoCloseOutputStream(stdin)) {
            final String cmd = "echo " + delay + " > /proc/sys/net/" + family + "/neigh/"
                    + mIfaceName + "/delay_first_probe_time";
            output.write(cmd.getBytes());
        }
        // Setting a value to sysctl doesn't have any output.
        final BufferedReader reader =
                new BufferedReader(new FileReader(stdout.getFileDescriptor()));
        assertNull(reader.readLine());

        // Check if the sysctl "delay_first_probe_time" has updated.
        final String delay_first_probe_time = getOneLineCommandOutput(
                "su root cat /proc/sys/net/" + family + "/neigh/" + mIfaceName
                        + "/delay_first_probe_time");
        assertEquals(delay, Integer.parseInt(delay_first_probe_time));
    }

    private int getNeighborParameterUcastSolicit(String ifaceName) throws IOException {
        final String ucast_solicit = getOneLineCommandOutput(
                "su root cat /proc/sys/net/ipv6/neigh/" + ifaceName + "/ucast_solicit");
        return Integer.parseInt(ucast_solicit);
    }

    private MacAddress getIfaceMacAddr(String ifaceName) throws IOException {
        // InterfaceParams.getByName requires CAP_NET_ADMIN: read the mac address with the shell
        final String strMacAddr = getOneLineCommandOutput(
                "su root cat /sys/class/net/" + ifaceName + "/address");
        return MacAddress.fromString(strMacAddr);
    }

    private String getOneLineCommandOutput(String cmd) throws IOException {
        try (ParcelFileDescriptor fd = InstrumentationRegistry.getInstrumentation()
                .getUiAutomation().executeShellCommand(cmd);
             BufferedReader reader = new BufferedReader(new FileReader(fd.getFileDescriptor()))) {
            return reader.readLine();
        }
    }

    private void enableRealAlarm(String cmdName) {
        doAnswer((inv) -> {
            final Context context = InstrumentationRegistry.getTargetContext();
            final AlarmManager alarmManager = context.getSystemService(AlarmManager.class);
            alarmManager.setExact(inv.getArgument(0), inv.getArgument(1), inv.getArgument(2),
                    inv.getArgument(3), inv.getArgument(4));
            return null;
        }).when(mAlarm).setExact(anyInt(), anyLong(), eq(cmdName), any(OnAlarmListener.class),
                any(Handler.class));
    }

    private IpClient makeIpClient() throws Exception {
        IpClient ipc =
                new IpClient(mContext, mIfaceName, mCb, mNetworkStackServiceManager, mDependencies);
        // Wait for IpClient to enter its initial state. Otherwise, additional setup steps or tests
        // that mock IpClient's dependencies might interact with those mocks while IpClient is
        // starting. This would cause UnfinishedStubbingExceptions as mocks cannot be interacted
        // with while they are being stubbed.
        HandlerUtils.waitForIdle(ipc.getHandler(), TEST_TIMEOUT_MS);
        return ipc;
    }

    private void setUpIpClient() throws Exception {
        final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
        final IBinder netdIBinder =
                (IBinder) inst.getContext().getSystemService(Context.NETD_SERVICE);
        mNetd = spy(INetd.Stub.asInterface(netdIBinder));
        when(mContext.getSystemService(eq(Context.NETD_SERVICE))).thenReturn(netdIBinder);
        assertNotNull(mNetd);

        mIpc = makeIpClient();

        // Tell the IpMemoryStore immediately to answer any question about network attributes with a
        // null response. Otherwise, the DHCP client will wait for two seconds before starting,
        // while its query to the IpMemoryStore times out.
        // This does not affect any test that makes the mock memory store return results, because
        // unlike when(), it is documented that doAnswer() can be called more than once, to change
        // the behaviour of a mock in the middle of a test.
        doAnswer(invocation -> {
            final String l2Key = invocation.getArgument(0);
            ((OnNetworkAttributesRetrievedListener) invocation.getArgument(1))
                    .onNetworkAttributesRetrieved(new Status(SUCCESS), l2Key, null);
            return null;
        }).when(mIpMemoryStore).retrieveNetworkAttributes(any(), any());

        disableIpv6ProvisioningDelays();
    }

    private <T> T verifyWithTimeout(InOrder inOrder, T t) {
        if (inOrder != null) {
            return inOrder.verify(t, timeout(TEST_TIMEOUT_MS));
        } else {
            return verify(t, timeout(TEST_TIMEOUT_MS));
        }
    }

    private void expectAlarmCancelled(InOrder inOrder, OnAlarmListener listener) {
        inOrder.verify(mAlarm, timeout(TEST_TIMEOUT_MS)).cancel(eq(listener));
    }

    private OnAlarmListener expectAlarmSet(InOrder inOrder, String tagMatch, long afterSeconds,
            Handler handler) {
        // Allow +/- 3 seconds to prevent flaky tests.
        final long when = SystemClock.elapsedRealtime() + afterSeconds * 1000;
        final long min = when - 3 * 1000;
        final long max = when + 3 * 1000;
        ArgumentCaptor<OnAlarmListener> captor = ArgumentCaptor.forClass(OnAlarmListener.class);
        verifyWithTimeout(inOrder, mAlarm).setExact(
                anyInt(), longThat(x -> x >= min && x <= max),
                contains(tagMatch), captor.capture(), eq(handler));
        return captor.getValue();
    }

    private OnAlarmListener expectAlarmSet(InOrder inOrder, String tagMatch, int afterSeconds) {
        return expectAlarmSet(inOrder, tagMatch, (long) afterSeconds, mIpc.getHandler());
    }

    private boolean isDhcpPacket(final byte[] packet) {
        final ByteBuffer buffer = ByteBuffer.wrap(packet);

        // check the packet length
        if (packet.length < DHCP_HEADER_OFFSET) return false;

        // check the source port and dest port in UDP header
        buffer.position(UDP_SRC_PORT_OFFSET);
        final short udpSrcPort = buffer.getShort();
        final short udpDstPort = buffer.getShort();
        if (udpSrcPort != DHCP_CLIENT || udpDstPort != DHCP_SERVER) return false;

        // check DHCP message type
        buffer.position(DHCP_MESSAGE_OP_CODE_OFFSET);
        final byte dhcpOpCode = buffer.get();
        if (dhcpOpCode != DHCP_BOOTREQUEST) return false;

        // check DHCP magic cookie
        buffer.position(DHCP_OPTION_MAGIC_COOKIE_OFFSET);
        final int dhcpMagicCookie = buffer.getInt();
        if (dhcpMagicCookie != DHCP_MAGIC_COOKIE) return false;

        return true;
    }

    private boolean isDhcp6Packet(final byte[] packet) {
        final ByteBuffer buffer = ByteBuffer.wrap(packet);

        // check the packet length
        if (packet.length < DHCP6_HEADER_OFFSET) return false;

        // check Ethernet header
        final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buffer);
        if (ethHdr.etherType != ETH_P_IPV6) {
            return false;
        }

        // check IPv6 header
        final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buffer);
        final int version = (ipv6Hdr.vtf >> 28) & 0x0F;
        if (version != 6) {
            return false;
        }
        if (ipv6Hdr.nextHeader != IPPROTO_UDP) {
            return false;
        }
        if (!ipv6Hdr.dstIp.equals(ALL_DHCP_RELAY_AGENTS_AND_SERVERS)) {
            return false;
        }
        mClientIpAddress = ipv6Hdr.srcIp;

        // check the source port and dest port in UDP header
        final short udpSrcPort = buffer.getShort();
        final short udpDstPort = buffer.getShort();
        return (udpSrcPort == DHCP6_CLIENT_PORT && udpDstPort == DHCP6_SERVER_PORT);
    }

    private ArpPacket parseArpPacketOrNull(final byte[] packet) {
        try {
            return ArpPacket.parseArpPacket(packet, packet.length);
        } catch (ArpPacket.ParseException e) {
            return null;
        }
    }

    private NeighborAdvertisement parseNeighborAdvertisementOrNull(final byte[] packet) {
        try {
            return NeighborAdvertisement.parse(packet, packet.length);
        } catch (NeighborAdvertisement.ParseException e) {
            return null;
        }
    }

    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,
            final String domainName, final List<String> domainSearchList) {
        return DhcpPacket.buildOfferPacket(DhcpPacket.ENCAP_L2, packet.getTransactionId(),
                false /* broadcast */, SERVER_ADDR, INADDR_ANY /* relayIp */,
                clientAddress /* yourIp */, packet.getClientMac(), leaseTimeSec,
                NETMASK /* netMask */, BROADCAST_ADDR /* bcAddr */,
                Collections.singletonList(SERVER_ADDR) /* gateways */,
                Collections.singletonList(SERVER_ADDR) /* dnsServers */,
                SERVER_ADDR /* dhcpServerIdentifier */, domainName, HOSTNAME,
                false /* metered */, mtu, captivePortalUrl, ipv6OnlyWaitTime, domainSearchList);
    }

    private static ByteBuffer buildDhcpOfferPacket(final DhcpPacket packet,
            final Inet4Address clientAddress, final Integer leaseTimeSec, final short mtu,
            final String captivePortalUrl) {
        return buildDhcpOfferPacket(packet, clientAddress, leaseTimeSec, mtu, captivePortalUrl,
                null /* ipv6OnlyWaitTime */, null /* domainName */, null /* domainSearchList */);
    }

    private static ByteBuffer buildDhcpAckPacket(final DhcpPacket packet,
            final Inet4Address clientAddress, final Integer leaseTimeSec, final short mtu,
            final boolean rapidCommit, final String captivePortalApiUrl,
            final Integer ipv6OnlyWaitTime, final String domainName,
            final List<String> domainSearchList) {
        return DhcpPacket.buildAckPacket(DhcpPacket.ENCAP_L2, packet.getTransactionId(),
                false /* broadcast */, SERVER_ADDR, INADDR_ANY /* relayIp */,
                clientAddress /* yourIp */, CLIENT_ADDR /* requestIp */, packet.getClientMac(),
                leaseTimeSec, NETMASK /* netMask */, BROADCAST_ADDR /* bcAddr */,
                Collections.singletonList(SERVER_ADDR) /* gateways */,
                Collections.singletonList(SERVER_ADDR) /* dnsServers */,
                SERVER_ADDR /* dhcpServerIdentifier */, domainName, HOSTNAME,
                false /* metered */, mtu, rapidCommit, captivePortalApiUrl, ipv6OnlyWaitTime,
                domainSearchList);
    }

    private static ByteBuffer buildDhcpAckPacket(final DhcpPacket packet,
            final Inet4Address clientAddress, final Integer leaseTimeSec, final short mtu,
            final boolean rapidCommit, final String captivePortalApiUrl) {
        return buildDhcpAckPacket(packet, clientAddress, leaseTimeSec, mtu, rapidCommit,
                captivePortalApiUrl, null /* ipv6OnlyWaitTime */, null /* domainName */,
                null /* domainSearchList */);
    }

    private static ByteBuffer buildDhcpNakPacket(final DhcpPacket packet, final String message) {
        return DhcpPacket.buildNakPacket(DhcpPacket.ENCAP_L2, packet.getTransactionId(),
            SERVER_ADDR /* serverIp */, INADDR_ANY /* relayIp */, packet.getClientMac(),
            false /* broadcast */, message);
    }

    private static ByteBuffer buildDhcp6Packet(final ByteBuffer payload, final MacAddress clientMac,
            final Inet6Address clientIp) throws Exception {
        final ByteBuffer buffer = PacketBuilder.allocate(true /* hasEther */, IPPROTO_IPV6,
                IPPROTO_UDP, payload.limit());
        final PacketBuilder pb = new PacketBuilder(buffer);

        pb.writeL2Header(ROUTER_MAC /* srcMac */, clientMac /* dstMac */, (short) ETH_P_IPV6);
        pb.writeIpv6Header(0x60000000 /* version=6, traffic class=0, flow label=0 */,
                (byte) IPPROTO_UDP, (short) 64 /* hop limit */, ROUTER_LINK_LOCAL /* srcIp */,
                clientIp /* dstIp */);
        pb.writeUdpHeader((short) DHCP6_SERVER_PORT /*src port */,
                (short) DHCP6_CLIENT_PORT /* dst port */);
        buffer.put(payload);
        return pb.finalizePacket();
    }

    private static ByteBuffer buildDhcp6Advertise(final Dhcp6Packet solicit, final byte[] iapd,
            final byte[] clientMac, final Inet6Address clientIp) throws Exception {
        final ByteBuffer advertise = Dhcp6Packet.buildAdvertisePacket(solicit.getTransactionId(),
                iapd, solicit.getClientDuid(), ROUTER_DUID);
        return buildDhcp6Packet(advertise, MacAddress.fromBytes(clientMac), clientIp);
    }

    private static ByteBuffer buildDhcp6Reply(final Dhcp6Packet request, final byte[] iapd,
            final byte[] clientMac, final Inet6Address clientIp, boolean rapidCommit)
            throws Exception {
        final ByteBuffer reply = Dhcp6Packet.buildReplyPacket(request.getTransactionId(),
                iapd, request.getClientDuid(), ROUTER_DUID, rapidCommit);
        return buildDhcp6Packet(reply, MacAddress.fromBytes(clientMac), clientIp);
    }

    private void sendArpReply(final byte[] dstMac, final byte[] srcMac, final Inet4Address targetIp,
            final Inet4Address senderIp) throws IOException {
        final ByteBuffer packet = ArpPacket.buildArpPacket(dstMac, srcMac, targetIp.getAddress(),
                dstMac /* target HW address */, senderIp.getAddress(), (short) ARP_REPLY);
        mPacketReader.sendResponse(packet);
    }

    private void sendArpProbe() throws IOException {
        final ByteBuffer packet = ArpPacket.buildArpPacket(DhcpPacket.ETHER_BROADCAST /* dst */,
                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);
    }

    private void sendGratuitousArp(MacAddress srcMac, Inet4Address targetIp) throws IOException {
        sendArpReply(ETHER_BROADCAST /* dstMac */, srcMac.toByteArray() /* srcMac */, targetIp,
                targetIp /* sender IP */);
    }

    private void startIpClientProvisioning(final ProvisioningConfiguration cfg) throws Exception {
        mIIpClient.startProvisioning(cfg.toStableParcelable());
    }

    private void startIpClientProvisioning(final boolean shouldReplyRapidCommitAck,
            final boolean isPreconnectionEnabled,
            final boolean isDhcpIpConflictDetectEnabled,
            final String displayName,
            final ScanResultInfo scanResultInfo,
            final Layer2Information layer2Info)
                    throws Exception {
        ProvisioningConfiguration.Builder prov = new ProvisioningConfiguration.Builder()
                .withoutIpReachabilityMonitor()
                .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);
        if (scanResultInfo != null) prov.withScanResultInfo(scanResultInfo);

        setDhcpFeatures(shouldReplyRapidCommitAck, isDhcpIpConflictDetectEnabled);

        startIpClientProvisioning(prov.build());
        if (!isPreconnectionEnabled) {
            verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true);
        }
        verify(mCb, never()).onProvisioningFailure(any());
    }

    private void startIpClientProvisioning(final boolean isDhcpRapidCommitEnabled,
            final boolean isPreconnectionEnabled,
            final boolean isDhcpIpConflictDetectEnabled) throws Exception {
        startIpClientProvisioning(isDhcpRapidCommitEnabled,
                isPreconnectionEnabled, isDhcpIpConflictDetectEnabled,
                null /* displayName */, null /* ScanResultInfo */, null /* layer2Info */);
    }

    private void assertIpMemoryStoreNetworkAttributes(final Integer leaseTimeSec,
            final long startTime, final int mtu) {
        final NetworkAttributes na = getStoredNetworkAttributes(TEST_L2KEY, TEST_TIMEOUT_MS);
        assertNotNull(na);
        assertEquals(CLIENT_ADDR, na.assignedV4Address);
        if (leaseTimeSec == null || leaseTimeSec.intValue() == DhcpPacket.INFINITE_LEASE) {
            assertEquals(Long.MAX_VALUE, na.assignedV4AddressExpiry.longValue());
        } else {
            // check the lease expiry's scope
            final long upperBound = startTime + 7_200_000; // start timestamp + 2h
            final long lowerBound = startTime + 3_600_000; // start timestamp + 1h
            final long expiry = na.assignedV4AddressExpiry;
            assertTrue(upperBound > expiry);
            assertTrue(lowerBound < expiry);
        }
        assertEquals(Collections.singletonList(SERVER_ADDR), na.dnsAddresses);
        assertEquals(new Integer(mtu), na.mtu);
    }

    private void assertIpMemoryNeverStoreNetworkAttributes() {
        assertIpMemoryNeverStoreNetworkAttributes(TEST_L2KEY, TEST_TIMEOUT_MS);
    }

    private void assertHostname(final boolean expectSendHostname,
            final String hostname, final String hostnameAfterTransliteration,
            final List<DhcpPacket> packetList) throws Exception {
        for (DhcpPacket packet : packetList) {
            if (!expectSendHostname || hostname == null) {
                assertNull(packet.getHostname());
            } else {
                assertEquals(hostnameAfterTransliteration, packet.getHostname());
            }
        }
    }

    // Helper method to complete DHCP 2-way or 4-way handshake
    private List<DhcpPacket> performDhcpHandshake(final boolean isSuccessLease,
            final Integer leaseTimeSec, final boolean shouldReplyRapidCommitAck, final int mtu,
            final boolean isDhcpIpConflictDetectEnabled,
            final String captivePortalApiUrl, final String displayName,
            final ScanResultInfo scanResultInfo, final Layer2Information layer2Info)
            throws Exception {
        startIpClientProvisioning(shouldReplyRapidCommitAck,
                false /* isPreconnectionEnabled */, isDhcpIpConflictDetectEnabled,
                displayName, scanResultInfo, layer2Info);
        return handleDhcpPackets(isSuccessLease, leaseTimeSec, shouldReplyRapidCommitAck, mtu,
                captivePortalApiUrl);
    }

    private List<DhcpPacket> handleDhcpPackets(final boolean isSuccessLease,
            final Integer leaseTimeSec, final boolean shouldReplyRapidCommitAck, final int mtu,
            final String captivePortalApiUrl) throws Exception {
        return handleDhcpPackets(isSuccessLease, leaseTimeSec, shouldReplyRapidCommitAck,
                mtu, captivePortalApiUrl, null /* ipv6OnlyWaitTime */,
                null /* domainName */, null /* domainSearchList */);
    }

    private List<DhcpPacket> handleDhcpPackets(final boolean isSuccessLease,
            final Integer leaseTimeSec, final boolean shouldReplyRapidCommitAck, final int mtu,
            final String captivePortalApiUrl, final Integer ipv6OnlyWaitTime,
            final String domainName, final List<String> domainSearchList) throws Exception {
        final List<DhcpPacket> packetList = new ArrayList<>();
        DhcpPacket packet;
        while ((packet = getNextDhcpPacket()) != null) {
            packetList.add(packet);
            if (packet instanceof DhcpDiscoverPacket) {
                if (shouldReplyRapidCommitAck) {
                    mPacketReader.sendResponse(buildDhcpAckPacket(packet, CLIENT_ADDR, leaseTimeSec,
                              (short) mtu, true /* rapidCommit */, captivePortalApiUrl,
                              ipv6OnlyWaitTime, domainName, domainSearchList));
                } else {
                    mPacketReader.sendResponse(buildDhcpOfferPacket(packet, CLIENT_ADDR,
                            leaseTimeSec, (short) mtu, captivePortalApiUrl, ipv6OnlyWaitTime,
                            domainName, domainSearchList));
                }
            } else if (packet instanceof DhcpRequestPacket) {
                final ByteBuffer byteBuffer = isSuccessLease
                        ? buildDhcpAckPacket(packet, CLIENT_ADDR, leaseTimeSec, (short) mtu,
                                false /* rapidCommit */, captivePortalApiUrl, ipv6OnlyWaitTime,
                                domainName, domainSearchList)
                        : buildDhcpNakPacket(packet, "duplicated request IP address");
                mPacketReader.sendResponse(byteBuffer);
            } else {
                fail("invalid DHCP packet");
            }

            // wait for reply to DHCPOFFER packet if disabling rapid commit option
            if (shouldReplyRapidCommitAck || !(packet instanceof DhcpDiscoverPacket)) {
                return packetList;
            }
        }
        fail("No DHCPREQUEST received on interface");
        return packetList;
    }

    private List<DhcpPacket> performDhcpHandshake(final boolean isSuccessLease,
            final Integer leaseTimeSec, final boolean isDhcpRapidCommitEnabled, final int mtu,
            final boolean isDhcpIpConflictDetectEnabled) throws Exception {
        return performDhcpHandshake(isSuccessLease, leaseTimeSec, isDhcpRapidCommitEnabled,
                mtu, isDhcpIpConflictDetectEnabled,
                null /* captivePortalApiUrl */, null /* displayName */, null /* scanResultInfo */,
                null /* layer2Info */);
    }

    private List<DhcpPacket> performDhcpHandshake() throws Exception {
        return performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
                false /* shouldReplyRapidCommitAck */,
                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
    }

    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 packet;
    }

    private Dhcp6Packet getNextDhcp6Packet(final long timeout) throws Exception {
        byte[] packet;
        while ((packet = mDhcp6PacketReadHead.getValue()
                .poll(timeout, this::isDhcp6Packet)) != null) {
            // Strip the Ethernet/IPv6/UDP headers, only keep DHCPv6 message payload for decode.
            final byte[] payload =
                    Arrays.copyOfRange(packet, DHCP6_HEADER_OFFSET, packet.length);
            final Dhcp6Packet dhcp6Packet = Dhcp6Packet.decode(payload, payload.length);
            if (dhcp6Packet != null) return dhcp6Packet;
        }
        return null;
    }

    private Dhcp6Packet getNextDhcp6Packet() throws Exception {
        final Dhcp6Packet packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS);
        assertNotNull("No expected DHCPv6 packet received on interface within timeout", packet);
        return packet;
    }

    private DhcpPacket getReplyFromDhcpLease(final NetworkAttributes na, boolean timeout)
            throws Exception {
        doAnswer(invocation -> {
            if (timeout) return null;
            ((OnNetworkAttributesRetrievedListener) invocation.getArgument(1))
                    .onNetworkAttributesRetrieved(new Status(SUCCESS), TEST_L2KEY, na);
            return null;
        }).when(mIpMemoryStore).retrieveNetworkAttributes(eq(TEST_L2KEY), any());
        startIpClientProvisioning(false /* shouldReplyRapidCommitAck */,
                false /* isPreconnectionEnabled */,
                false /* isDhcpIpConflictDetectEnabled */);
        return getNextDhcpPacket();
    }

    private void removeTestInterface(final FileDescriptor fd) {
        try {
            Os.close(fd);
        } catch (ErrnoException e) {
            fail("Fail to close file descriptor: " + e);
        }
    }

    private void verifyAfterIpClientShutdown() throws RemoteException {
        final LinkProperties emptyLp = new LinkProperties();
        emptyLp.setInterfaceName(mIfaceName);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(emptyLp);
    }

    // Verify IPv4-only provisioning success. No need to verify IPv4 provisioning when below cases
    // happen:
    // 1. if there's a failure lease, onProvisioningSuccess() won't be called;
    // 2. if duplicated IPv4 address detection is enabled, verify TIMEOUT will affect ARP packets
    //    capture running in other test cases.
    // 3. if IPv6 is enabled, e.g. withoutIPv6() isn't called when starting provisioning.
    private LinkProperties verifyIPv4OnlyProvisioningSuccess(
            final Collection<InetAddress> addresses) throws Exception {
        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
        final LinkProperties lp = captor.getValue();
        assertNotNull(lp);
        assertNotEquals(0, lp.getDnsServers().size());
        assertEquals(addresses.size(), lp.getAddresses().size());
        assertTrue(lp.getAddresses().containsAll(addresses));
        assertTrue(hasRouteTo(lp, IPV4_TEST_SUBNET_PREFIX)); // IPv4 directly-connected route
        assertTrue(hasRouteTo(lp, IPV4_ANY_ADDRESS_PREFIX)); // IPv4 default route
        return lp;
    }

    private void doRestoreInitialMtuTest(final boolean shouldChangeMtu,
            final boolean shouldRemoveTestInterface) throws Exception {
        final long currentTime = System.currentTimeMillis();
        int mtu = TEST_DEFAULT_MTU;

        if (shouldChangeMtu) mtu = TEST_MIN_MTU;
        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
                false /* shouldReplyRapidCommitAck */,
                mtu, false /* isDhcpIpConflictDetectEnabled */);
        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, mtu);

        if (shouldChangeMtu) {
            // Pretend that ConnectivityService set the MTU.
            NetlinkUtils.setInterfaceMtu(mDependencies.getInterfaceParams(mIfaceName).index, mtu);
            assertEquals(NetworkInterface.getByName(mIfaceName).getMTU(), mtu);
        }

        // Sometimes, IpClient receives an update with an empty LinkProperties during startup,
        // when the link-local address is deleted after interface bringup. Reset expectations
        // here to ensure that verifyAfterIpClientShutdown does not fail because it sees two
        // empty LinkProperties changes instead of one.
        reset(mCb);

        if (shouldRemoveTestInterface) removeTestInterface(mTapFd);
        try {
            mIpc.shutdown();
            awaitIpClientShutdown();
            if (!shouldRemoveTestInterface) {
                // Verify that MTU indeed has been restored or not.
                assertEquals(NetworkInterface.getByName(mIfaceName).getMTU(), TEST_DEFAULT_MTU);
            }
            verifyAfterIpClientShutdown();
        } catch (Exception e) {
            fail("Exception should not have been thrown after shutdown: " + e);
        }
    }

    private DhcpPacket assertDiscoverPacketOnPreconnectionStart() throws Exception {
        final ArgumentCaptor<List<Layer2PacketParcelable>> l2PacketList =
                ArgumentCaptor.forClass(List.class);

        verify(mCb, timeout(TEST_TIMEOUT_MS)).onPreconnectionStart(l2PacketList.capture());
        final byte[] payload = l2PacketList.getValue().get(0).payload;
        DhcpPacket packet = DhcpPacket.decodeFullPacket(payload, payload.length, ENCAP_L2);
        assertTrue(packet instanceof DhcpDiscoverPacket);
        assertArrayEquals(INADDR_BROADCAST.getAddress(),
                Arrays.copyOfRange(payload, IPV4_DST_ADDR_OFFSET, IPV4_DST_ADDR_OFFSET + 4));
        return packet;
    }

    private void doIpClientProvisioningWithPreconnectionTest(
            final boolean shouldReplyRapidCommitAck, final boolean shouldAbortPreconnection,
            final boolean shouldFirePreconnectionTimeout,
            final boolean timeoutBeforePreconnectionComplete) throws Exception {
        final long currentTime = System.currentTimeMillis();
        startIpClientProvisioning(shouldReplyRapidCommitAck,
                true /* isDhcpPreConnectionEnabled */,
                false /* isDhcpIpConflictDetectEnabled */);
        DhcpPacket packet = assertDiscoverPacketOnPreconnectionStart();
        final int preconnDiscoverTransId = packet.getTransactionId();

        if (shouldAbortPreconnection) {
            if (shouldFirePreconnectionTimeout && timeoutBeforePreconnectionComplete) {
                mDependencies.mDhcpClient.sendMessage(DhcpClient.CMD_TIMEOUT);
            }

            mIpc.notifyPreconnectionComplete(false /* abort */);
            HandlerUtils.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);

            if (shouldFirePreconnectionTimeout && !timeoutBeforePreconnectionComplete) {
                mDependencies.mDhcpClient.sendMessage(DhcpClient.CMD_TIMEOUT);
            }

            // Either way should get DhcpClient go back to INIT state, and broadcast
            // DISCOVER with new transaction ID.
            packet = getNextDhcpPacket();
            assertTrue(packet instanceof DhcpDiscoverPacket);
            assertTrue(packet.getTransactionId() != preconnDiscoverTransId);
        } else if (shouldFirePreconnectionTimeout && timeoutBeforePreconnectionComplete) {
            // If timeout fires before success preconnection, DhcpClient will go back to INIT state,
            // and broadcast DISCOVER with new transaction ID.
            mDependencies.mDhcpClient.sendMessage(DhcpClient.CMD_TIMEOUT);
            packet = getNextDhcpPacket();
            assertTrue(packet instanceof DhcpDiscoverPacket);
            assertTrue(packet.getTransactionId() != preconnDiscoverTransId);
            // any old response would be ignored due to mismatched transaction ID.
        }

        final short mtu = (short) TEST_DEFAULT_MTU;
        if (!shouldReplyRapidCommitAck) {
            mPacketReader.sendResponse(buildDhcpOfferPacket(packet, CLIENT_ADDR,
                    TEST_LEASE_DURATION_S, mtu, null /* captivePortalUrl */));
            packet = getNextDhcpPacket();
            assertTrue(packet instanceof DhcpRequestPacket);
        }
        mPacketReader.sendResponse(buildDhcpAckPacket(packet, CLIENT_ADDR, TEST_LEASE_DURATION_S,
                mtu, shouldReplyRapidCommitAck, null /* captivePortalUrl */));

        if (!shouldAbortPreconnection) {
            mIpc.notifyPreconnectionComplete(true /* success */);
            HandlerUtils.waitForIdle(mDependencies.mDhcpClient.getHandler(), TEST_TIMEOUT_MS);

            // If timeout fires after successful preconnection, right now DhcpClient will have
            // already entered BOUND state, the delayed CMD_TIMEOUT command would be ignored. So
            // this case should be very rare, because the timeout alarm is cancelled when state
            // machine exits from Preconnecting state.
            if (shouldFirePreconnectionTimeout && !timeoutBeforePreconnectionComplete) {
                mDependencies.mDhcpClient.sendMessage(DhcpClient.CMD_TIMEOUT);
            }
        }
        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true);
        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
    }

    private ArpPacket getNextArpPacket(final long timeout) throws Exception {
        byte[] packet;
        while ((packet = mArpPacketReadHead.getValue().poll(timeout, p -> true)) != null) {
            final ArpPacket arpPacket = parseArpPacketOrNull(packet);
            if (arpPacket != null) return arpPacket;
        }
        return null;
    }

    private ArpPacket getNextArpPacket() throws Exception {
        final ArpPacket packet = getNextArpPacket(PACKET_TIMEOUT_MS);
        assertNotNull("No expected ARP packet received on interface within timeout", packet);
        return packet;
    }

    private void assertArpPacket(final ArpPacket packet) {
        assertEquals(packet.opCode, ARP_REQUEST);
        assertEquals(packet.targetIp, CLIENT_ADDR);
        assertTrue(Arrays.equals(packet.senderHwAddress.toByteArray(), mClientMac));
    }

    private void assertArpProbe(final ArpPacket packet) {
        assertArpPacket(packet);
        assertEquals(packet.senderIp, INADDR_ANY);
    }

    private void assertArpAnnounce(final ArpPacket packet) {
        assertArpPacket(packet);
        assertEquals(packet.senderIp, CLIENT_ADDR);
    }

    private void assertArpRequest(final ArpPacket packet, final Inet4Address targetIp) {
        assertEquals(packet.opCode, ARP_REQUEST);
        assertEquals(packet.senderIp, CLIENT_ADDR);
        assertEquals(packet.targetIp, targetIp);
        assertTrue(Arrays.equals(packet.targetHwAddress.toByteArray(),
                MacAddress.fromString("00:00:00:00:00:00").toByteArray()));
        assertTrue(Arrays.equals(packet.senderHwAddress.toByteArray(), mClientMac));
    }

    private void assertGratuitousARP(final ArpPacket packet) {
        assertEquals(packet.opCode, ARP_REPLY);
        assertEquals(packet.senderIp, CLIENT_ADDR);
        assertEquals(packet.targetIp, CLIENT_ADDR);
        assertTrue(Arrays.equals(packet.senderHwAddress.toByteArray(), mClientMac));
        assertTrue(Arrays.equals(packet.targetHwAddress.toByteArray(), ETHER_BROADCAST));
    }

    private void doIpAddressConflictDetectionTest(final boolean causeIpAddressConflict,
            final boolean shouldReplyRapidCommitAck, final boolean isDhcpIpConflictDetectEnabled,
            final boolean shouldResponseArpReply) throws Exception {
        final long currentTime = System.currentTimeMillis();

        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
                shouldReplyRapidCommitAck,
                TEST_DEFAULT_MTU, isDhcpIpConflictDetectEnabled);

        // If we receive an ARP packet here, it's guaranteed to be from IP conflict detection,
        // because at this time the test interface does not have an IP address and therefore
        // won't send ARP for anything.
        if (causeIpAddressConflict) {
            final ArpPacket arpProbe = getNextArpPacket();
            assertArpProbe(arpProbe);

            if (shouldResponseArpReply) {
                sendArpReply(mClientMac /* dstMac */, ROUTER_MAC_BYTES /* srcMac */,
                        INADDR_ANY /* target IP */, CLIENT_ADDR /* sender IP */);
            } else {
                sendArpProbe();
            }
            final DhcpPacket packet = getNextDhcpPacket();
            assertTrue(packet instanceof DhcpDeclinePacket);
            assertEquals(packet.mServerIdentifier, SERVER_ADDR);
            assertEquals(packet.mRequestedIp, CLIENT_ADDR);

            verify(mCb, never()).onProvisioningFailure(any());
            assertIpMemoryNeverStoreNetworkAttributes();
        } else if (isDhcpIpConflictDetectEnabled) {
            final List<ArpPacket> packetList = new ArrayList<ArpPacket>();
            // Total sent ARP packets should be 5 (3 ARP Probes + 2 ARP Announcements)
            ArpPacket packet;
            while ((packet = getNextArpPacket(TEST_TIMEOUT_MS)) != null) {
                packetList.add(packet);
            }
            assertEquals(5, packetList.size());
            assertArpProbe(packetList.get(0));
            assertArpAnnounce(packetList.get(3));
        } else {
            verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
            assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime,
                    TEST_DEFAULT_MTU);
        }
    }

    @Test @SignatureRequiredTest(reason = "InterfaceParams.getByName requires CAP_NET_ADMIN")
    public void testInterfaceParams() throws Exception {
        InterfaceParams params = InterfaceParams.getByName(mIfaceName);
        assertNotNull(params);
        assertEquals(mIfaceName, params.name);
        assertTrue(params.index > 0);
        assertNotNull(params.macAddr);
        assertTrue(params.hasMacAddress);

        //  Check interface "lo".
        params = InterfaceParams.getByName("lo");
        assertNotNull(params);
        assertEquals("lo", params.name);
        assertTrue(params.index > 0);
        assertNotNull(params.macAddr);
        assertFalse(params.hasMacAddress);
    }

    @Test
    public void testDhcpInit() throws Exception {
        startIpClientProvisioning(false /* shouldReplyRapidCommitAck */,
                false /* isPreconnectionEnabled */,
                false /* isDhcpIpConflictDetectEnabled */);
        final DhcpPacket packet = getNextDhcpPacket();
        assertTrue(packet instanceof DhcpDiscoverPacket);
    }

    @Test
    public void testHandleSuccessDhcpLease() throws Exception {
        final long currentTime = System.currentTimeMillis();
        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
                false /* shouldReplyRapidCommitAck */,
                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
    }

    @Test
    public void testHandleFailureDhcpLease() throws Exception {
        performDhcpHandshake(false /* isSuccessLease */, TEST_LEASE_DURATION_S,
                false /* shouldReplyRapidCommitAck */,
                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);

        verify(mCb, never()).onProvisioningSuccess(any());
        assertIpMemoryNeverStoreNetworkAttributes();
    }

    @Test
    public void testHandleInfiniteLease() throws Exception {
        final long currentTime = System.currentTimeMillis();
        performDhcpHandshake(true /* isSuccessLease */, INFINITE_LEASE,
                false /* shouldReplyRapidCommitAck */,
                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
        assertIpMemoryStoreNetworkAttributes(INFINITE_LEASE, currentTime, TEST_DEFAULT_MTU);
    }

    @Test
    public void testHandleNoLease() throws Exception {
        final long currentTime = System.currentTimeMillis();
        performDhcpHandshake(true /* isSuccessLease */, null /* no lease time */,
                false /* shouldReplyRapidCommitAck */,
                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
        assertIpMemoryStoreNetworkAttributes(null, currentTime, TEST_DEFAULT_MTU);
    }

    @Test
    public void testHandleRapidCommitOption() throws Exception {
        final long currentTime = System.currentTimeMillis();
        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
                true /* shouldReplyRapidCommitAck */,
                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
    }

    @Test
    public void testRollbackFromRapidCommitOption() throws Exception {
        startIpClientProvisioning(true /* isDhcpRapidCommitEnabled */,
                false /* isPreConnectionEnabled */,
                false /* isDhcpIpConflictDetectEnabled */);

        final List<DhcpPacket> discoverList = new ArrayList<DhcpPacket>();
        DhcpPacket packet;
        do {
            packet = getNextDhcpPacket();
            assertTrue(packet instanceof DhcpDiscoverPacket);
            discoverList.add(packet);
        } while (discoverList.size() < 4);

        // Check the only first 3 DHCPDISCOVERs take rapid commit option.
        assertTrue(discoverList.get(0).mRapidCommit);
        assertTrue(discoverList.get(1).mRapidCommit);
        assertTrue(discoverList.get(2).mRapidCommit);
        assertFalse(discoverList.get(3).mRapidCommit);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpClientStartWithCachedInfiniteLease() throws Exception {
        final DhcpPacket packet = getReplyFromDhcpLease(
                new NetworkAttributes.Builder()
                    .setAssignedV4Address(CLIENT_ADDR)
                    .setAssignedV4AddressExpiry(Long.MAX_VALUE) // lease is always valid
                    .setMtu(new Integer(TEST_DEFAULT_MTU))
                    .setCluster(TEST_CLUSTER)
                    .setDnsAddresses(Collections.singletonList(SERVER_ADDR))
                    .build(), false /* timeout */);
        assertTrue(packet instanceof DhcpRequestPacket);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpClientStartWithCachedExpiredLease() throws Exception {
        final DhcpPacket packet = getReplyFromDhcpLease(
                 new NetworkAttributes.Builder()
                    .setAssignedV4Address(CLIENT_ADDR)
                    .setAssignedV4AddressExpiry(EXPIRED_LEASE)
                    .setMtu(new Integer(TEST_DEFAULT_MTU))
                    .setCluster(TEST_CLUSTER)
                    .setDnsAddresses(Collections.singletonList(SERVER_ADDR))
                    .build(), false /* timeout */);
        assertTrue(packet instanceof DhcpDiscoverPacket);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpClientStartWithNullRetrieveNetworkAttributes() throws Exception {
        final DhcpPacket packet = getReplyFromDhcpLease(null /* na */, false /* timeout */);
        assertTrue(packet instanceof DhcpDiscoverPacket);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpClientStartWithTimeoutRetrieveNetworkAttributes() throws Exception {
        final DhcpPacket packet = getReplyFromDhcpLease(
                new NetworkAttributes.Builder()
                    .setAssignedV4Address(CLIENT_ADDR)
                    .setAssignedV4AddressExpiry(System.currentTimeMillis() + 3_600_000)
                    .setMtu(new Integer(TEST_DEFAULT_MTU))
                    .setCluster(TEST_CLUSTER)
                    .setDnsAddresses(Collections.singletonList(SERVER_ADDR))
                    .build(), true /* timeout */);
        assertTrue(packet instanceof DhcpDiscoverPacket);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpClientStartWithCachedLeaseWithoutIPAddress() throws Exception {
        final DhcpPacket packet = getReplyFromDhcpLease(
                new NetworkAttributes.Builder()
                    .setMtu(new Integer(TEST_DEFAULT_MTU))
                    .setCluster(TEST_CLUSTER)
                    .setDnsAddresses(Collections.singletonList(SERVER_ADDR))
                    .build(), false /* timeout */);
        assertTrue(packet instanceof DhcpDiscoverPacket);
    }

    @Test
    public void testDhcpClientRapidCommitEnabled() throws Exception {
        startIpClientProvisioning(true /* shouldReplyRapidCommitAck */,
                false /* isPreconnectionEnabled */,
                false /* isDhcpIpConflictDetectEnabled */);
        final DhcpPacket packet = getNextDhcpPacket();
        assertTrue(packet instanceof DhcpDiscoverPacket);
    }

    @Test
    public void testDhcpServerInLinkProperties() throws Exception {
        performDhcpHandshake();
        ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
        assertEquals(SERVER_ADDR, captor.getValue().getDhcpServerAddress());
    }

    private void createTestNetworkAgentAndRegister(final LinkProperties lp) throws Exception {
        final Context context = InstrumentationRegistry.getInstrumentation().getContext();
        final ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
        final NetworkSpecifier testNetworkSpecifier =
                CompatUtil.makeTestNetworkSpecifier(mIfaceName);
        final TestableNetworkCallback cb = new TestableNetworkCallback();

        // Requesting a network make sure the NetworkAgent is alive during the whole life cycle of
        // requested network.
        cm.requestNetwork(new NetworkRequest.Builder()
                .removeCapability(NET_CAPABILITY_TRUSTED)
                .removeCapability(NET_CAPABILITY_INTERNET)
                .addTransportType(TRANSPORT_TEST)
                .setNetworkSpecifier(testNetworkSpecifier)
                .build(), cb);
        mNetworkAgent = new TestableNetworkAgent(context, mNetworkAgentThread.getLooper(),
                new NetworkCapabilities.Builder()
                        .removeCapability(NET_CAPABILITY_TRUSTED)
                        .removeCapability(NET_CAPABILITY_INTERNET)
                        .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
                        .addCapability(NET_CAPABILITY_NOT_ROAMING)
                        .addCapability(NET_CAPABILITY_NOT_VPN)
                        .addCapability(NET_CAPABILITY_NOT_RESTRICTED)
                        .addTransportType(TRANSPORT_TEST)
                        .setNetworkSpecifier(testNetworkSpecifier)
                        .build(),
                lp,
                new NetworkAgentConfig.Builder().build());
        mNetworkAgent.register();
        mNetworkAgent.markConnected();
        cb.expectAvailableThenValidatedCallbacks(mNetworkAgent.getNetwork(), TEST_TIMEOUT_MS);
    }

    private void assertReceivedDhcpRequestPacketCount() throws Exception {
        final List<DhcpPacket> packetList = new ArrayList<>();
        DhcpPacket packet;
        while ((packet = getNextDhcpPacket(PACKET_TIMEOUT_MS)) != null) {
            assertDhcpRequestForReacquire(packet);
            packetList.add(packet);
        }
        assertEquals(1, packetList.size());
    }

    private LinkProperties prepareDhcpReacquireTest() throws Exception {
        mNetworkAgentThread =
                new HandlerThread(IpClientIntegrationTestCommon.class.getSimpleName());
        mNetworkAgentThread.start();

        final long currentTime = System.currentTimeMillis();
        setFeatureEnabled(NetworkStackUtils.DHCP_SLOW_RETRANSMISSION_VERSION, true);
        performDhcpHandshake(true /* isSuccessLease */,
                TEST_LEASE_DURATION_S, false /* isDhcpRapidCommitEnabled */, TEST_DEFAULT_MTU,
                false /* isDhcpIpConflictDetectEnabled */);
        final LinkProperties lp =
                verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
        return lp;
    }

    private OnAlarmListener runDhcpRenewTest(final Handler handler, final LinkProperties lp,
            final InOrder inOrder) throws Exception {
        // Create a NetworkAgent and register it to ConnectivityService with IPv4 LinkProperties,
        // then ConnectivityService will call netd API to configure the IPv4 route on the kernel,
        // otherwise, unicast DHCPREQUEST cannot be sent out due to no route to host(EHOSTUNREACH).
        runAsShell(MANAGE_TEST_NETWORKS, () -> createTestNetworkAgentAndRegister(lp));

        // DHCP client is in BOUND state right now, simulate the renewal via triggering renew alarm
        // which should happen at T1. E.g. lease duration is 3600s, T1 = lease_duration * 0.5(1800s)
        // T2 = lease_duration * 0.875(3150s).
        final OnAlarmListener renewAlarm = expectAlarmSet(inOrder, "RENEW", 1800, handler);
        final OnAlarmListener rebindAlarm = expectAlarmSet(inOrder, "REBIND", 3150, handler);

        // Trigger renew alarm and force DHCP client enter RenewingState. Device needs to start
        // the ARP resolution for the fake DHCP server IPv4 address before sending the unicast
        // DHCPREQUEST out, wait for the unicast ARP request and respond to it with ARP reply,
        // otherwise, DHCPREQUEST still cannot be sent out due to that there is no correct ARP
        // table for the dest IPv4 address.
        handler.post(() -> renewAlarm.onAlarm());
        final ArpPacket request = getNextArpPacket();
        assertArpRequest(request, SERVER_ADDR);
        sendArpReply(request.senderHwAddress.toByteArray() /* dst */, ROUTER_MAC_BYTES /* srcMac */,
                request.senderIp /* target IP */, SERVER_ADDR /* sender IP */);
        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);

        // Verify there should be only one unicast DHCPREQUESTs to be received per RFC2131.
        assertReceivedDhcpRequestPacketCount();

        return rebindAlarm;
    }

    @Test @SignatureRequiredTest(reason = "Need to mock the DHCP renew/rebind alarms")
    public void testDhcpRenew() throws Exception {
        final LinkProperties lp = prepareDhcpReacquireTest();
        final InOrder inOrder = inOrder(mAlarm);
        runDhcpRenewTest(mDependencies.mDhcpClient.getHandler(), lp, inOrder);
    }

    @Test @SignatureRequiredTest(reason = "Need to mock the DHCP renew/rebind alarms")
    public void testDhcpRebind() throws Exception {
        final LinkProperties lp = prepareDhcpReacquireTest();
        final Handler handler = mDependencies.mDhcpClient.getHandler();
        final InOrder inOrder = inOrder(mAlarm);
        final OnAlarmListener rebindAlarm = runDhcpRenewTest(handler, lp, inOrder);

        // Trigger rebind alarm and forece DHCP client enter RebindingState. DHCP client sends
        // broadcast DHCPREQUEST to nearby servers, then check how many DHCPREQUEST packets are
        // retransmitted within PACKET_TIMEOUT_MS(5s), there should be only one DHCPREQUEST
        // captured per RFC2131.
        handler.post(() -> rebindAlarm.onAlarm());
        assertReceivedDhcpRequestPacketCount();
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testRestoreInitialInterfaceMtu() throws Exception {
        doRestoreInitialMtuTest(true /* shouldChangeMtu */, false /* shouldRemoveTestInterface */);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testRestoreInitialInterfaceMtu_WithoutMtuChange() throws Exception {
        doRestoreInitialMtuTest(false /* shouldChangeMtu */, false /* shouldRemoveTestInterface */);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testRestoreInitialInterfaceMtu_NotFoundInterfaceWhenStopping() throws Exception {
        doRestoreInitialMtuTest(true /* shouldChangeMtu */, true /* shouldRemoveTestInterface */);
    }

    @Test
    public void testRestoreInitialInterfaceMtu_NotFoundInterfaceWhenStartingProvisioning()
            throws Exception {
        removeTestInterface(mTapFd);
        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIpReachabilityMonitor()
                .withoutIPv6()
                .build();

        startIpClientProvisioning(config);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningFailure(any());
        verify(mCb, never()).setNeighborDiscoveryOffload(true);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testRestoreInitialInterfaceMtu_stopIpClientAndRestart() throws Exception {
        long currentTime = System.currentTimeMillis();

        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
                false /* shouldReplyRapidCommitAck */,
                TEST_MIN_MTU, false /* isDhcpIpConflictDetectEnabled */);
        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_MIN_MTU);

        // Pretend that ConnectivityService set the MTU.
        NetlinkUtils.setInterfaceMtu(
                mDependencies.getInterfaceParams(mIfaceName).index, TEST_MIN_MTU);
        assertEquals(NetworkInterface.getByName(mIfaceName).getMTU(), TEST_MIN_MTU);

        reset(mCb);
        reset(mIpMemoryStore);

        // Stop IpClient and then restart provisioning immediately.
        mIpc.stop();
        currentTime = System.currentTimeMillis();
        // Intend to set mtu option to 0, then verify that won't influence interface mtu restore.
        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
                false /* shouldReplyRapidCommitAck */,
                0 /* mtu */, false /* isDhcpIpConflictDetectEnabled */);
        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, 0 /* mtu */);
        assertEquals(NetworkInterface.getByName(mIfaceName).getMTU(), TEST_DEFAULT_MTU);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testRestoreInitialInterfaceMtu_removeInterfaceAndAddback() throws Exception {
        doAnswer(invocation -> {
            final LinkProperties lp = invocation.getArgument(0);
            assertEquals(lp.getInterfaceName(), mIfaceName);
            assertEquals(0, lp.getLinkAddresses().size());
            assertEquals(0, lp.getDnsServers().size());

            mDependencies.simulateInterfaceRecover();
            return null;
        }).when(mCb).onProvisioningFailure(any());

        final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIpReachabilityMonitor()
                .withoutIPv6()
                .build();

        // Intend to remove the tap interface and force IpClient throw provisioning failure
        // due to that interface is not found.
        removeTestInterface(mTapFd);
        assertNull(InterfaceParams.getByName(mIfaceName));

        startIpClientProvisioning(config);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningFailure(any());

        // Make sure everything queued by this test was processed (e.g. transition to StoppingState
        // from ClearingIpAddressState) and tearDown will check if IpClient exits normally or crash.
        HandlerUtils.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);
    }

    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) type;
    }

    private boolean isRouterSolicitation(final byte[] packetBytes) {
        return isIcmpv6PacketOfType(packetBytes, ICMPV6_ROUTER_SOLICITATION);
    }

    private boolean isNeighborAdvertisement(final byte[] packetBytes) {
        return isIcmpv6PacketOfType(packetBytes, ICMPV6_NEIGHBOR_ADVERTISEMENT);
    }

    private boolean isNeighborSolicitation(final byte[] packetBytes) {
        return isIcmpv6PacketOfType(packetBytes, ICMPV6_NEIGHBOR_SOLICITATION);
    }

    private NeighborAdvertisement getNextNeighborAdvertisement() throws ParseException {
        final byte[] packet = mPacketReader.popPacket(PACKET_TIMEOUT_MS,
                this::isNeighborAdvertisement);
        if (packet == null) return null;

        final NeighborAdvertisement na = parseNeighborAdvertisementOrNull(packet);
        assertNotNull("Invalid neighbour advertisement received", na);
        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));
    }

    private void sendGratuitousNeighborAdvertisement(final MacAddress srcMac,
            final Inet6Address srcIp, final Inet6Address targetIp) throws Exception {
        int flags = NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER | NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
        final Inet6Address dstIp = IPV6_ADDR_ALL_NODES_MULTICAST;
        final MacAddress dstMac = NetworkStackUtils.ipv6MulticastToEthernetMulticast(dstIp);
        final ByteBuffer packet =
                NeighborAdvertisement.build(srcMac, dstMac, srcIp, dstIp, flags, targetIp);
        mPacketReader.sendResponse(packet);
    }

    private void sendRouterAdvertisement(boolean waitForRs, short lifetime, int valid,
            int preferred) throws Exception {
        final ByteBuffer pio = buildPioOption(valid, preferred, TEST_IPV6_PREFIX);
        final ByteBuffer rdnss = buildRdnssOption(3600, IPV6_OFF_LINK_DNS_SERVER);
        sendRouterAdvertisement(waitForRs, lifetime, pio, rdnss);
    }

    private void sendRouterAdvertisement(boolean waitForRs, short lifetime,
            ByteBuffer... options) throws Exception {
        final ByteBuffer ra = buildRaPacket(lifetime, options);
        if (waitForRs) {
            waitForRouterSolicitation();
        }
        mPacketReader.sendResponse(ra);
    }

    private void sendBasicRouterAdvertisement(boolean waitForRs) throws Exception {
        sendRouterAdvertisement(waitForRs, (short) 1800 /* lifetime */, 3600 /* valid */,
                1800 /* preferred */);
    }

    private void sendRouterAdvertisementWithZeroRouterLifetime() throws Exception {
        sendRouterAdvertisement(false /* waitForRs */, (short) 0 /* lifetime */, 3600 /* valid */,
                1800 /* preferred */);
    }

    // TODO: move this and the following method to a common location and use them in ApfTest.
    private static ByteBuffer buildPioOption(int valid, int preferred, String prefixString)
            throws Exception {
        return buildPioOption(valid, preferred, (byte) (PIO_FLAG_ON_LINK | PIO_FLAG_AUTONOMOUS),
                prefixString);
    }

    private static ByteBuffer buildPioOption(int valid, int preferred, byte flags,
            String prefixString) throws Exception {
        return PrefixInformationOption.build(new IpPrefix(prefixString), flags, valid, preferred);
    }

    private static ByteBuffer buildRdnssOption(int lifetime, String... servers) throws Exception {
        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 =
                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 {
        return buildRaPacket((short) 1800, options);
    }

    private static ByteBuffer buildRaPacket(final String prefix, final String dnsServer,
            int validLifetime, int preferredLifetime, int dnsLifetime, boolean shouldIncludeSlla)
            throws Exception {
        final ByteBuffer pio = buildPioOption(validLifetime, preferredLifetime, prefix);
        final ByteBuffer rdnss = buildRdnssOption(dnsLifetime, dnsServer);
        final List<ByteBuffer> options = new ArrayList<ByteBuffer>();
        options.add(pio);
        options.add(rdnss);
        if (shouldIncludeSlla) options.add(buildSllaOption());
        return buildRaPacket(options.toArray(new ByteBuffer[options.size()]));
    }

    private void disableIpv6ProvisioningDelays() throws Exception {
        // Speed up the test by disabling DAD and removing router_solicitation_delay.
        // We don't need to restore the default value because the interface is removed in tearDown.
        // TODO: speed up further by not waiting for RS but keying off first IPv6 packet.
        mNetd.setProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "router_solicitation_delay", "0");
        mNetd.setProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "dad_transmits", "0");
    }

    private void assertHasAddressThat(String msg, LinkProperties lp,
            Predicate<LinkAddress> condition) {
        for (LinkAddress addr : lp.getLinkAddresses()) {
            if (condition.test(addr)) {
                return;
            }
        }
        fail(msg + " not found in: " + lp);
    }

    private boolean hasFlag(LinkAddress addr, int flag) {
        return (addr.getFlags() & flag) == flag;
    }

    private boolean isPrivacyAddress(LinkAddress addr) {
        return addr.isGlobalPreferred() && hasFlag(addr, IFA_F_TEMPORARY);
    }

    private boolean isStablePrivacyAddress(LinkAddress addr) {
        return addr.isGlobalPreferred() && hasFlag(addr, IFA_F_STABLE_PRIVACY);
    }

    private LinkProperties doIpv6OnlyProvisioning() throws Exception {
        final InOrder inOrder = inOrder(mCb);
        final ByteBuffer ra = buildRaPacket(TEST_IPV6_PREFIX, IPV6_OFF_LINK_DNS_SERVER,
                3600 /* validLifetime */, 1800 /* preferredLifetime */, 3600 /* dnsLifetime */,
                true /* shouldIncludeSlla */);
        return doIpv6OnlyProvisioning(inOrder, ra);
    }

    private LinkProperties doIpv6OnlyProvisioning(InOrder inOrder, ByteBuffer ra) throws Exception {
        waitForRouterSolicitation();
        // Waiting for the IPv6 link-local address to appear, which ensures that the IPv6 stack
        // is enabled and can process Router Advertisements.
        // TODO: why this is only required for certain platform (see b/428036745). Otherwise, it
        // seems that the kernel missed the RA and failed to get the default IPv6 route.
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(argThat(
                x -> hasIpv6LinkLocalAddress(x)
        ));
        mPacketReader.sendResponse(ra);

        // The lambda below needs to write a LinkProperties to a local variable, but lambdas cannot
        // write to non-final local variables. So declare a final variable to write to.
        final AtomicReference<LinkProperties> lpRef = new AtomicReference<>();

        ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verifyWithTimeout(inOrder, mCb).onProvisioningSuccess(captor.capture());
        lpRef.set(captor.getValue());

        // Sometimes provisioning completes as soon as the link-local and the stable address appear,
        // before the privacy address appears. If so, wait here for the LinkProperties update that
        // contains all three address. Otherwise, future calls to verify() might get confused.
        if (captor.getValue().getLinkAddresses().size() == 2) {
            verifyWithTimeout(inOrder, mCb).onLinkPropertiesChange(argThat(lp -> {
                lpRef.set(lp);
                return lp.getLinkAddresses().size() == 3;
            }));
        }

        LinkProperties lp = lpRef.get();
        assertEquals("Should have 3 IPv6 addresses after provisioning: " + lp,
                3, lp.getLinkAddresses().size());
        assertHasAddressThat("link-local address", lp, x -> x.getAddress().isLinkLocalAddress());
        assertHasAddressThat("privacy address", lp, this::isPrivacyAddress);
        assertHasAddressThat("stable privacy address", lp, this::isStablePrivacyAddress);

        return lp;
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testRaRdnss() throws Exception {
        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIpReachabilityMonitor()
                .withoutIPv4()
                .build();
        startIpClientProvisioning(config);

        InOrder inOrder = inOrder(mCb);
        ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);

        final String dnsServer = "2001:4860:4860::64";
        final String lowlifeDnsServer = "2001:4860:4860::6464";

        final ByteBuffer pio = buildPioOption(600, 300, "2001:db8:1::/64");
        ByteBuffer rdnss1 = buildRdnssOption(60, lowlifeDnsServer);
        ByteBuffer rdnss2 = buildRdnssOption(600, dnsServer);
        ByteBuffer ra = buildRaPacket(pio, rdnss1, rdnss2);

        LinkProperties lp = doIpv6OnlyProvisioning(inOrder, ra);

        // Expect that DNS servers with lifetimes below CONFIG_ACCEPT_RA_MIN_LFT are not accepted.
        assertNotNull(lp);
        assertEquals(1, lp.getDnsServers().size());
        assertTrue(lp.getDnsServers().contains(InetAddress.getByName(dnsServer)));

        // If the RDNSS lifetime is above the minimum, the DNS server is accepted.
        rdnss1 = buildRdnssOption(67, lowlifeDnsServer);
        ra = buildRaPacket(pio, rdnss1, rdnss2);
        mPacketReader.sendResponse(ra);
        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(captor.capture());
        lp = captor.getValue();
        assertNotNull(lp);
        assertEquals(2, lp.getDnsServers().size());
        assertTrue(lp.getDnsServers().contains(InetAddress.getByName(dnsServer)));
        assertTrue(lp.getDnsServers().contains(InetAddress.getByName(lowlifeDnsServer)));

        // Expect that setting RDNSS lifetime of 0 causes loss of provisioning.
        rdnss1 = buildRdnssOption(0, dnsServer);
        rdnss2 = buildRdnssOption(0, lowlifeDnsServer);
        ra = buildRaPacket(pio, rdnss1, rdnss2);
        mPacketReader.sendResponse(ra);

        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningFailure(captor.capture());
        lp = captor.getValue();
        assertNotNull(lp);
        assertEquals(0, lp.getDnsServers().size());
        reset(mCb);
    }

    private void runRaRdnssIpv6LinkLocalDnsTest() throws Exception {
        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIpReachabilityMonitor()
                .withoutIPv4()
                .build();
        startIpClientProvisioning(config);

        final ByteBuffer ra = buildRaPacket(TEST_IPV6_PREFIX,
                ROUTER_LINK_LOCAL.getHostAddress() /* an IPv6 link-local DNS server */,
                600 /* validLifetime */, 300 /* preferredLifetime */, 600 /* dnsLifetime */,
                true /* shouldIncludeSlla */);

        waitForRouterSolicitation();
        mPacketReader.sendResponse(ra);
    }

    @Test
    public void testRaRdnss_Ipv6LinkLocalDns() throws Exception {
        runRaRdnssIpv6LinkLocalDnsTest();
        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
        final LinkProperties lp = captor.getValue();
        assertNotNull(lp);
        assertEquals(1, lp.getDnsServers().size());
        assertEquals(ROUTER_LINK_LOCAL, (Inet6Address) lp.getDnsServers().get(0));
        assertTrue(lp.isIpv6Provisioned());
    }

    private void expectNat64PrefixUpdate(InOrder inOrder, IpPrefix expected) throws Exception {
        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(
                argThat(lp -> Objects.equals(expected, lp.getNat64Prefix())));

    }

    private void expectNoNat64PrefixUpdate(InOrder inOrder, IpPrefix unchanged) throws Exception {
        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS).times(0)).onLinkPropertiesChange(argThat(
                lp -> !Objects.equals(unchanged, lp.getNat64Prefix())));

    }

    @Test
    @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testPref64Option() throws Exception {
        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIpReachabilityMonitor()
                .withoutIPv4()
                .build();
        startIpClientProvisioning(config);

        final IpPrefix prefix = new IpPrefix("64:ff9b::/96");
        final IpPrefix otherPrefix = new IpPrefix("2001:db8:64::/96");

        final ByteBuffer pio = buildPioOption(600, 300, "2001:db8:1::/64");
        ByteBuffer rdnss = buildRdnssOption(600, IPV6_OFF_LINK_DNS_SERVER);
        ByteBuffer pref64 = new StructNdOptPref64(prefix, 600).toByteBuffer();
        ByteBuffer ra = buildRaPacket(pio, rdnss, pref64);

        // The NAT64 prefix might be detected before or after provisioning success.
        // Don't test order between these two events.
        LinkProperties lp = doIpv6OnlyProvisioning(null /*inOrder*/, ra);
        expectAlarmSet(null /*inOrder*/, "PREF64", 600);

        // From now on expect events in order.
        InOrder inOrder = inOrder(mCb, mAlarm);
        if (lp.getNat64Prefix() != null) {
            assertEquals(prefix, lp.getNat64Prefix());
        } else {
            expectNat64PrefixUpdate(inOrder, prefix);
        }

        // Increase the lifetime and expect the prefix not to change.
        pref64 = new StructNdOptPref64(prefix, 1800).toByteBuffer();
        ra = buildRaPacket(pio, rdnss, pref64);
        mPacketReader.sendResponse(ra);
        OnAlarmListener pref64Alarm = expectAlarmSet(inOrder, "PREF64", 1800);
        expectNoNat64PrefixUpdate(inOrder, prefix);
        reset(mCb, mAlarm);

        // Reduce the lifetime and expect to reschedule expiry.
        pref64 = new StructNdOptPref64(prefix, 1500).toByteBuffer();
        ra = buildRaPacket(pio, rdnss, pref64);
        mPacketReader.sendResponse(ra);
        pref64Alarm = expectAlarmSet(inOrder, "PREF64", 1496);
        expectNoNat64PrefixUpdate(inOrder, prefix);
        reset(mCb, mAlarm);

        // Withdraw the prefix and expect it to be set to null.
        pref64 = new StructNdOptPref64(prefix, 0).toByteBuffer();
        ra = buildRaPacket(pio, rdnss, pref64);
        mPacketReader.sendResponse(ra);
        expectAlarmCancelled(inOrder, pref64Alarm);
        expectNat64PrefixUpdate(inOrder, null);
        reset(mCb, mAlarm);

        // Re-announce the prefix.
        pref64 = new StructNdOptPref64(prefix, 600).toByteBuffer();
        ra = buildRaPacket(pio, rdnss, pref64);
        mPacketReader.sendResponse(ra);
        expectAlarmSet(inOrder, "PREF64", 600);
        expectNat64PrefixUpdate(inOrder, prefix);
        reset(mCb, mAlarm);

        // Announce two prefixes. Don't expect any update because if there is already a NAT64
        // prefix, any new prefix is ignored.
        ByteBuffer otherPref64 = new StructNdOptPref64(otherPrefix, 1200).toByteBuffer();
        ra = buildRaPacket(pio, rdnss, pref64, otherPref64);
        mPacketReader.sendResponse(ra);
        expectAlarmSet(inOrder, "PREF64", 600);
        expectNoNat64PrefixUpdate(inOrder, prefix);
        reset(mCb, mAlarm);

        // Withdraw the old prefix and continue to announce the new one. Expect a prefix change.
        pref64 = new StructNdOptPref64(prefix, 0).toByteBuffer();
        ra = buildRaPacket(pio, rdnss, pref64, otherPref64);
        mPacketReader.sendResponse(ra);
        expectAlarmCancelled(inOrder, pref64Alarm);
        // Need a different OnAlarmListener local variable because posting it to the handler in the
        // lambda below requires it to be final.
        final OnAlarmListener lastAlarm = expectAlarmSet(inOrder, "PREF64", 1200);
        expectNat64PrefixUpdate(inOrder, otherPrefix);
        reset(mCb, mAlarm);

        // Simulate prefix expiry.
        mIpc.getHandler().post(() -> lastAlarm.onAlarm());
        expectAlarmCancelled(inOrder, pref64Alarm);
        expectNat64PrefixUpdate(inOrder, null);

        // Announce a non-/96 prefix and expect it to be ignored.
        IpPrefix invalidPrefix = new IpPrefix("64:ff9b::/64");
        pref64 = new StructNdOptPref64(invalidPrefix, 1200).toByteBuffer();
        ra = buildRaPacket(pio, rdnss, pref64);
        mPacketReader.sendResponse(ra);
        expectNoNat64PrefixUpdate(inOrder, invalidPrefix);

        // Re-announce the prefix.
        pref64 = new StructNdOptPref64(prefix, 600).toByteBuffer();
        ra = buildRaPacket(pio, rdnss, pref64);
        mPacketReader.sendResponse(ra);
        final OnAlarmListener clearAlarm = expectAlarmSet(inOrder, "PREF64", 600);
        expectNat64PrefixUpdate(inOrder, prefix);
        reset(mCb, mAlarm);

        // Check that the alarm is cancelled when IpClient is stopped.
        mIpc.stop();
        HandlerUtils.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);
        expectAlarmCancelled(inOrder, clearAlarm);
        expectNat64PrefixUpdate(inOrder, null);

        // Check that even if the alarm was already in the message queue while it was cancelled, it
        // is safely ignored.
        mIpc.getHandler().post(() -> clearAlarm.onAlarm());
        HandlerUtils.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);
    }

    private void addIpAddressAndWaitForIt(final String iface) throws Exception {
        final String addr1 = "192.0.2.99";
        final String addr2 = "192.0.2.3";
        final int prefixLength = 26;

        // IpClient gets IP addresses directly from netlink instead of from netd, just
        // add the addresses directly and wait to see if IpClient has seen the address.
        mNetd.interfaceAddAddress(iface, addr1, prefixLength);
        mNetd.interfaceAddAddress(iface, addr2, prefixLength);

        // Wait for IpClient to process the addition of the address.
        HandlerUtils.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);
    }

    private void doIPv4OnlyProvisioningAndExitWithLeftAddress() throws Exception {
        final long currentTime = System.currentTimeMillis();
        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
                false /* shouldReplyRapidCommitAck */,
                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);

        // Stop IpClient and expect a final LinkProperties callback with an empty LP.
        mIIpClient.stop();
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(argThat(
                x -> x.getAddresses().size() == 0
                        && x.getRoutes().size() == 0
                        && x.getDnsServers().size() == 0));
        reset(mCb);

        // Pretend that something else (e.g., Tethering) used the interface and left an IP address
        // configured on it. When IpClient starts, it must clear this address before proceeding.
        // The address must be noticed before startProvisioning is called, or IpClient will
        // immediately declare provisioning success due to the presence of an IPv4 address.
        // The address must be IPv4 because IpClient clears IPv6 addresses on startup.
        addIpAddressAndWaitForIt(mIfaceName);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testIpClientClearingIpAddressState() throws Exception {
        doIPv4OnlyProvisioningAndExitWithLeftAddress();

        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIpReachabilityMonitor()
                .build();
        startIpClientProvisioning(config);

        sendBasicRouterAdvertisement(true /*waitForRs*/);

        // Check that the IPv4 addresses configured earlier are not in LinkProperties...
        ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
        assertFalse(captor.getValue().hasIpv4Address());

        // ... or configured on the interface.
        InterfaceConfigurationParcel cfg = mNetd.interfaceGetCfg(mIfaceName);
        assertEquals("0.0.0.0", cfg.ipv4Addr);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testIpClientClearingIpAddressState_enablePreconnection() throws Exception {
        doIPv4OnlyProvisioningAndExitWithLeftAddress();

        // Enter ClearingIpAddressesState to clear the remaining IPv4 addresses and transition to
        // PreconnectionState instead of RunningState.
        startIpClientProvisioning(false /* shouldReplyRapidCommitAck */,
                true /* isDhcpPreConnectionEnabled */,
                false /* isDhcpIpConflictDetectEnabled */);
        assertDiscoverPacketOnPreconnectionStart();

        // Force to enter RunningState.
        mIpc.notifyPreconnectionComplete(false /* abort */);
        HandlerUtils.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpClientPreconnection_success() throws Exception {
        doIpClientProvisioningWithPreconnectionTest(true /* shouldReplyRapidCommitAck */,
                false /* shouldAbortPreconnection */, false /* shouldFirePreconnectionTimeout */,
                false /* timeoutBeforePreconnectionComplete */);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpClientPreconnection_SuccessWithoutRapidCommit() throws Exception {
        doIpClientProvisioningWithPreconnectionTest(false /* shouldReplyRapidCommitAck */,
                false /* shouldAbortPreconnection */, false /* shouldFirePreconnectionTimeout */,
                false /* timeoutBeforePreconnectionComplete */);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpClientPreconnection_Abort() throws Exception {
        doIpClientProvisioningWithPreconnectionTest(true /* shouldReplyRapidCommitAck */,
                true /* shouldAbortPreconnection */, false /* shouldFirePreconnectionTimeout */,
                false /* timeoutBeforePreconnectionComplete */);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpClientPreconnection_AbortWithoutRapiCommit() throws Exception {
        doIpClientProvisioningWithPreconnectionTest(false /* shouldReplyRapidCommitAck */,
                true /* shouldAbortPreconnection */, false /* shouldFirePreconnectionTimeout */,
                false /* timeoutBeforePreconnectionComplete */);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpClientPreconnection_TimeoutBeforeAbort() throws Exception {
        doIpClientProvisioningWithPreconnectionTest(true /* shouldReplyRapidCommitAck */,
                true /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
                true /* timeoutBeforePreconnectionComplete */);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpClientPreconnection_TimeoutBeforeAbortWithoutRapidCommit()
            throws Exception {
        doIpClientProvisioningWithPreconnectionTest(false /* shouldReplyRapidCommitAck */,
                true /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
                true /* timeoutBeforePreconnectionComplete */);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpClientPreconnection_TimeoutafterAbort() throws Exception {
        doIpClientProvisioningWithPreconnectionTest(true /* shouldReplyRapidCommitAck */,
                true /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
                false /* timeoutBeforePreconnectionComplete */);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpClientPreconnection_TimeoutAfterAbortWithoutRapidCommit() throws Exception {
        doIpClientProvisioningWithPreconnectionTest(false /* shouldReplyRapidCommitAck */,
                true /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
                false /* timeoutBeforePreconnectionComplete */);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpClientPreconnection_TimeoutBeforeSuccess() throws Exception {
        doIpClientProvisioningWithPreconnectionTest(true /* shouldReplyRapidCommitAck */,
                false /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
                true /* timeoutBeforePreconnectionComplete */);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpClientPreconnection_TimeoutBeforeSuccessWithoutRapidCommit()
            throws Exception {
        doIpClientProvisioningWithPreconnectionTest(false /* shouldReplyRapidCommitAck */,
                false /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
                true /* timeoutBeforePreconnectionComplete */);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpClientPreconnection_TimeoutAfterSuccess() throws Exception {
        doIpClientProvisioningWithPreconnectionTest(true /* shouldReplyRapidCommitAck */,
                false /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
                false /* timeoutBeforePreconnectionComplete */);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpClientPreconnection_TimeoutAfterSuccessWithoutRapidCommit()
            throws Exception {
        doIpClientProvisioningWithPreconnectionTest(false /* shouldReplyRapidCommitAck */,
                false /* shouldAbortPreconnection */, true /* shouldFirePreconnectionTimeout */,
                false /* timeoutBeforePreconnectionComplete */);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpClientPreconnection_WithoutLayer2InfoWhenStartingProv() throws Exception {
        // For FILS connection, current bssid (also l2key and cluster) is still null when
        // starting provisioning since the L2 link hasn't been established yet. Ensure that
        // IpClient won't crash even if initializing an Layer2Info class with null members.
        ProvisioningConfiguration.Builder prov = new ProvisioningConfiguration.Builder()
                .withoutIpReachabilityMonitor()
                .withoutIPv6()
                .withPreconnection()
                .withLayer2Information(new Layer2Information(null /* l2key */, null /* cluster */,
                        null /* bssid */));

        startIpClientProvisioning(prov.build());
        assertDiscoverPacketOnPreconnectionStart();
        verify(mCb).setNeighborDiscoveryOffload(true);

        // Force IpClient transition to RunningState from PreconnectionState.
        mIIpClient.notifyPreconnectionComplete(false /* success */);
        HandlerUtils.waitForIdle(mDependencies.mDhcpClient.getHandler(), TEST_TIMEOUT_MS);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true);
    }

    @Test
    @SignatureRequiredTest(reason = "needs mocked alarm and access to IpClient handler thread")
    public void testDhcpClientPreconnection_DelayedAbortAndTransitToStoppedState()
            throws Exception {
        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIpReachabilityMonitor()
                .withPreconnection()
                .build();
        setDhcpFeatures(false /* shouldReplyRapidCommitAck */,
                false /* isDhcpIpConflictDetectEnabled */);
        startIpClientProvisioning(config);
        assertDiscoverPacketOnPreconnectionStart();

        // IpClient is in the PreconnectingState, simulate provisioning timeout event
        // and force IpClient state machine transit to StoppingState.
        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        final OnAlarmListener alarm = expectAlarmSet(null /* inOrder */, "TIMEOUT", 18,
                mIpc.getHandler());
        mIpc.getHandler().post(() -> alarm.onAlarm());

        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningFailure(captor.capture());
        final LinkProperties lp = captor.getValue();
        assertNotNull(lp);
        assertEquals(mIfaceName, lp.getInterfaceName());
        assertEquals(0, lp.getLinkAddresses().size());
        assertEquals(0, lp.getRoutes().size());
        assertEquals(0, lp.getMtu());
        assertEquals(0, lp.getDnsServers().size());

        // Send preconnection abort message, but IpClient should ignore it at this moment and
        // transit to StoppedState finally.
        mIpc.notifyPreconnectionComplete(false /* abort */);
        mIpc.stop();
        HandlerUtils.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);

        reset(mCb);

        // Start provisioning again to verify IpClient can process CMD_START correctly at
        // StoppedState.
        startIpClientProvisioning(false /* shouldReplyRapidCommitAck */,
                false /* isPreConnectionEnabled */,
                false /* isDhcpIpConflictDetectEnabled */);
        final DhcpPacket discover = getNextDhcpPacket();
        assertTrue(discover instanceof DhcpDiscoverPacket);
    }

    @Test
    public void testDhcpDecline_conflictByArpReply() throws Exception {
        doIpAddressConflictDetectionTest(true /* causeIpAddressConflict */,
                false /* shouldReplyRapidCommitAck */, true /* isDhcpIpConflictDetectEnabled */,
                true /* shouldResponseArpReply */);
    }

    @Test
    public void testDhcpDecline_conflictByArpProbe() throws Exception {
        doIpAddressConflictDetectionTest(true /* causeIpAddressConflict */,
                false /* shouldReplyRapidCommitAck */, true /* isDhcpIpConflictDetectEnabled */,
                false /* shouldResponseArpReply */);
    }

    @Test
    public void testDhcpDecline_EnableFlagWithoutIpConflict() throws Exception {
        doIpAddressConflictDetectionTest(false /* causeIpAddressConflict */,
                false /* shouldReplyRapidCommitAck */, true /* isDhcpIpConflictDetectEnabled */,
                false /* shouldResponseArpReply */);
    }

    @Test
    public void testDhcpDecline_WithoutIpConflict() throws Exception {
        doIpAddressConflictDetectionTest(false /* causeIpAddressConflict */,
                false /* shouldReplyRapidCommitAck */, false /* isDhcpIpConflictDetectEnabled */,
                false /* shouldResponseArpReply */);
    }

    @Test
    public void testDhcpDecline_WithRapidCommitWithoutIpConflict() throws Exception {
        doIpAddressConflictDetectionTest(false /* causeIpAddressConflict */,
                true /* shouldReplyRapidCommitAck */, false /* isDhcpIpConflictDetectEnabled */,
                false /* shouldResponseArpReply */);
    }

    @Test
    public void testDhcpDecline_WithRapidCommitConflictByArpReply() throws Exception {
        doIpAddressConflictDetectionTest(true /* causeIpAddressConflict */,
                true /* shouldReplyRapidCommitAck */, true /* isDhcpIpConflictDetectEnabled */,
                true /* shouldResponseArpReply */);
    }

    @Test
    public void testDhcpDecline_WithRapidCommitConflictByArpProbe() throws Exception {
        doIpAddressConflictDetectionTest(true /* causeIpAddressConflict */,
                true /* shouldReplyRapidCommitAck */, true /* isDhcpIpConflictDetectEnabled */,
                false /* shouldResponseArpReply */);
    }

    @Test
    public void testDhcpDecline_EnableFlagWithRapidCommitWithoutIpConflict() throws Exception {
        doIpAddressConflictDetectionTest(false /* causeIpAddressConflict */,
                true /* shouldReplyRapidCommitAck */, true /* isDhcpIpConflictDetectEnabled */,
                false /* shouldResponseArpReply */);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testHostname_enableConfig() throws Exception {
        mDependencies.setHostnameConfiguration(true /* isHostnameConfigurationEnabled */,
                TEST_HOST_NAME);

        final long currentTime = System.currentTimeMillis();
        final List<DhcpPacket> sentPackets = performDhcpHandshake(true /* isSuccessLease */,
                TEST_LEASE_DURATION_S, false /* isDhcpRapidCommitEnabled */, TEST_DEFAULT_MTU,
                false /* isDhcpIpConflictDetectEnabled */);

        assertEquals(2, sentPackets.size());
        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
        assertHostname(true, TEST_HOST_NAME, TEST_HOST_NAME_TRANSLITERATION, sentPackets);
        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testHostname_disableConfig() throws Exception {
        mDependencies.setHostnameConfiguration(false /* isHostnameConfigurationEnabled */,
                TEST_HOST_NAME);

        final long currentTime = System.currentTimeMillis();
        final List<DhcpPacket> sentPackets = performDhcpHandshake(true /* isSuccessLease */,
                TEST_LEASE_DURATION_S, false /* isDhcpRapidCommitEnabled */, TEST_DEFAULT_MTU,
                false /* isDhcpIpConflictDetectEnabled */);

        assertEquals(2, sentPackets.size());
        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
        assertHostname(false, TEST_HOST_NAME, TEST_HOST_NAME_TRANSLITERATION, sentPackets);
        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testHostname_enableConfigWithNullHostname() throws Exception {
        mDependencies.setHostnameConfiguration(true /* isHostnameConfigurationEnabled */,
                null /* hostname */);

        final long currentTime = System.currentTimeMillis();
        final List<DhcpPacket> sentPackets = performDhcpHandshake(true /* isSuccessLease */,
                TEST_LEASE_DURATION_S, false /* isDhcpRapidCommitEnabled */, TEST_DEFAULT_MTU,
                false /* isDhcpIpConflictDetectEnabled */);

        assertEquals(2, sentPackets.size());
        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
        assertHostname(true, null /* hostname */, null /* hostnameAfterTransliteration */,
                sentPackets);
        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
    }

    private LinkProperties runDhcpClientCaptivePortalApiTest(boolean featureEnabled,
            boolean serverSendsOption) throws Exception {
        startIpClientProvisioning(false /* shouldReplyRapidCommitAck */,
                false /* isPreConnectionEnabled */,
                false /* isDhcpIpConflictDetectEnabled */);
        final DhcpPacket discover = getNextDhcpPacket();
        assertTrue(discover instanceof DhcpDiscoverPacket);
        assertEquals(featureEnabled, discover.hasRequestedParam(DhcpPacket.DHCP_CAPTIVE_PORTAL));

        // Send Offer and handle Request -> Ack
        final String serverSentUrl = serverSendsOption ? TEST_CAPTIVE_PORTAL_URL : null;
        mPacketReader.sendResponse(buildDhcpOfferPacket(discover, CLIENT_ADDR,
                TEST_LEASE_DURATION_S, (short) TEST_DEFAULT_MTU, serverSentUrl));
        final int testMtu = 1345;
        handleDhcpPackets(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
                false /* shouldReplyRapidCommitAck */, testMtu, serverSentUrl);

        final Uri expectedUrl = featureEnabled && serverSendsOption
                ? Uri.parse(TEST_CAPTIVE_PORTAL_URL) : null;
        // LinkProperties will be updated multiple times. Wait for it to contain DHCP-obtained info,
        // such as MTU.
        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS).atLeastOnce()).onLinkPropertiesChange(
                argThat(lp -> lp.getMtu() == testMtu));

        // Ensure that the URL was set as expected in the callbacks.
        verify(mCb, atLeastOnce()).onLinkPropertiesChange(captor.capture());
        final LinkProperties expectedLp = captor.getAllValues().stream().findFirst().get();
        assertNotNull(expectedLp);
        assertEquals(expectedUrl, expectedLp.getCaptivePortalApiUrl());
        return expectedLp;
    }

    @Test
    public void testDhcpClientCaptivePortalApiEnabled() throws Exception {
        // Only run the test on platforms / builds where the API is enabled
        assumeTrue(CaptivePortalDataShimImpl.isSupported());
        runDhcpClientCaptivePortalApiTest(true /* featureEnabled */, true /* serverSendsOption */);
    }

    @Test
    public void testDhcpClientCaptivePortalApiEnabled_NoUrl() throws Exception {
        // Only run the test on platforms / builds where the API is enabled
        assumeTrue(CaptivePortalDataShimImpl.isSupported());
        runDhcpClientCaptivePortalApiTest(true /* featureEnabled */, false /* serverSendsOption */);
    }

    @Test
    public void testDhcpClientCaptivePortalApiEnabled_ParcelSensitiveFields() throws Exception {
        // Only run the test on platforms / builds where the API is enabled
        assumeTrue(CaptivePortalDataShimImpl.isSupported());
        LinkProperties lp = runDhcpClientCaptivePortalApiTest(true /* featureEnabled */,
                true /* serverSendsOption */);

        // Integration test process runs in the same process with network stack module, there
        // won't be any IPC call happened on IpClientCallbacks, manually run parcelingRoundTrip
        // to parcel and unparcel the LinkProperties to simulate what happens during the binder
        // call. In this case lp should contain the senstive data but mParcelSensitiveFields is
        // false after round trip.
        if (useNetworkStackSignature()) {
            lp = parcelingRoundTrip(lp);
        }
        final Uri expectedUrl = Uri.parse(TEST_CAPTIVE_PORTAL_URL);
        assertEquals(expectedUrl, lp.getCaptivePortalApiUrl());

        // Parcel and unparcel the captured LinkProperties, mParcelSensitiveFields is false,
        // CaptivePortalApiUrl should be null after parceling round trip.
        final LinkProperties unparceled = parcelingRoundTrip(lp);
        assertNull(unparceled.getCaptivePortalApiUrl());
    }

    @Test
    public void testDhcpClientCaptivePortalApiDisabled() throws Exception {
        // Only run the test on platforms / builds where the API is disabled
        assumeFalse(CaptivePortalDataShimImpl.isSupported());
        runDhcpClientCaptivePortalApiTest(false /* featureEnabled */, true /* serverSendsOption */);
    }

    private ScanResultInfo makeScanResultInfo(final int id, final String ssid,
            final String bssid, final byte[] oui, final byte type, final byte[] data) {
        final ByteBuffer payload = ByteBuffer.allocate(4 + data.length);
        payload.put(oui);
        payload.put(type);
        payload.put(data);
        payload.flip();
        final ScanResultInfo.InformationElement ie =
                new ScanResultInfo.InformationElement(id /* IE id */, payload);
        return new ScanResultInfo(ssid, bssid, Collections.singletonList(ie));
    }

    private ScanResultInfo makeScanResultInfo(final int id, final byte[] oui, final byte type) {
        byte[] data = new byte[10];
        new Random().nextBytes(data);
        return makeScanResultInfo(id, TEST_DEFAULT_SSID, TEST_DEFAULT_BSSID, oui, type, data);
    }

    private ScanResultInfo makeScanResultInfo(final String ssid, final String bssid) {
        byte[] data = new byte[10];
        new Random().nextBytes(data);
        return makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, ssid, bssid, TEST_AP_OUI,
                (byte) 0x06, data);
    }

    private void doUpstreamHotspotDetectionTest(final int id, final String displayName,
            final String ssid, final byte[] oui, final byte type, final byte[] data,
            final boolean expectMetered) throws Exception {
        final ScanResultInfo info = makeScanResultInfo(id, ssid, TEST_DEFAULT_BSSID, oui, type,
                data);
        final long currentTime = System.currentTimeMillis();
        final List<DhcpPacket> sentPackets = performDhcpHandshake(true /* isSuccessLease */,
                TEST_LEASE_DURATION_S, false /* isDhcpRapidCommitEnabled */, TEST_DEFAULT_MTU,
                false /* isDhcpIpConflictDetectEnabled */,
                null /* captivePortalApiUrl */, displayName, info /* scanResultInfo */,
                null /* layer2Info */);
        assertEquals(2, sentPackets.size());
        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));

        ArgumentCaptor<DhcpResultsParcelable> captor =
                ArgumentCaptor.forClass(DhcpResultsParcelable.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onNewDhcpResults(captor.capture());
        final DhcpResultsParcelable lease = captor.getValue();
        assertNotNull(lease);
        assertEquals(CLIENT_ADDR, lease.baseConfiguration.getIpAddress().getAddress());
        assertEquals(SERVER_ADDR, lease.baseConfiguration.getGateway());
        assertEquals(1, lease.baseConfiguration.getDnsServers().size());
        assertTrue(lease.baseConfiguration.getDnsServers().contains(SERVER_ADDR));
        assertEquals(SERVER_ADDR, InetAddresses.parseNumericAddress(lease.serverAddress));
        assertEquals(TEST_DEFAULT_MTU, lease.mtu);

        if (expectMetered) {
            assertEquals(lease.vendorInfo, DhcpPacket.VENDOR_INFO_ANDROID_METERED);
        } else {
            assertNull(lease.vendorInfo);
        }

        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
    }

    @Test
    public void testUpstreamHotspotDetection() throws Exception {
        byte[] data = new byte[10];
        new Random().nextBytes(data);
        doUpstreamHotspotDetectionTest(TEST_VENDOR_SPECIFIC_IE_ID, "\"ssid\"", "ssid",
                new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x06, data,
                true /* expectMetered */);
    }

    @Test
    public void testUpstreamHotspotDetection_incorrectIeId() throws Exception {
        byte[] data = new byte[10];
        new Random().nextBytes(data);
        doUpstreamHotspotDetectionTest(0xdc, "\"ssid\"", "ssid",
                new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x06, data,
                false /* expectMetered */);
    }

    @Test
    public void testUpstreamHotspotDetection_incorrectOUI() throws Exception {
        byte[] data = new byte[10];
        new Random().nextBytes(data);
        doUpstreamHotspotDetectionTest(TEST_VENDOR_SPECIFIC_IE_ID, "\"ssid\"", "ssid",
                new byte[] { (byte) 0x00, (byte) 0x1A, (byte) 0x11 }, (byte) 0x06, data,
                false /* expectMetered */);
    }

    @Test
    public void testUpstreamHotspotDetection_incorrectSsid() throws Exception {
        byte[] data = new byte[10];
        new Random().nextBytes(data);
        doUpstreamHotspotDetectionTest(TEST_VENDOR_SPECIFIC_IE_ID, "\"another ssid\"", "ssid",
                new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x06, data,
                false /* expectMetered */);
    }

    @Test
    public void testUpstreamHotspotDetection_incorrectType() throws Exception {
        byte[] data = new byte[10];
        new Random().nextBytes(data);
        doUpstreamHotspotDetectionTest(TEST_VENDOR_SPECIFIC_IE_ID, "\"ssid\"", "ssid",
                new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x0a, data,
                false /* expectMetered */);
    }

    @Test
    public void testUpstreamHotspotDetection_zeroLengthData() throws Exception {
        byte[] data = new byte[0];
        doUpstreamHotspotDetectionTest(TEST_VENDOR_SPECIFIC_IE_ID, "\"ssid\"", "ssid",
                new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x06, data,
                true /* expectMetered */);
    }

    private void forceLayer2Roaming() throws Exception {
        final Layer2InformationParcelable roamingInfo = new Layer2InformationParcelable();
        roamingInfo.bssid = MacAddress.fromString(TEST_DHCP_ROAM_BSSID);
        roamingInfo.l2Key = TEST_DHCP_ROAM_L2KEY;
        roamingInfo.cluster = TEST_DHCP_ROAM_CLUSTER;
        mIIpClient.updateLayer2Information(roamingInfo);
    }

    private void assertDhcpRequestForReacquire(final DhcpPacket packet) {
        assertTrue(packet instanceof DhcpRequestPacket);
        assertEquals(packet.mClientIp, CLIENT_ADDR);    // client IP
        assertNull(packet.mRequestedIp);                // requested IP option
        assertNull(packet.mServerIdentifier);           // server ID
    }

    private void doDhcpRoamingTest(final boolean hasMismatchedIpAddress, final String displayName,
            final MacAddress bssid, final boolean expectRoaming,
            final boolean shouldReplyNakOnRoam) throws Exception {
        long currentTime = System.currentTimeMillis();
        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.
            // Just return null and force state machine enter INIT state.
            final String l2Key = invocation.getArgument(0);
            ((OnNetworkAttributesRetrievedListener) invocation.getArgument(1))
                    .onNetworkAttributesRetrieved(new Status(SUCCESS), l2Key, null);
            return null;
        }).when(mIpMemoryStore).retrieveNetworkAttributes(eq(TEST_L2KEY), any());

        mDependencies.setHostnameConfiguration(true /* isHostnameConfigurationEnabled */,
                null /* hostname */);
        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
                false /* isDhcpRapidCommitEnabled */,
                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */,
                null /* captivePortalApiUrl */, displayName, null /* scanResultInfo */,
                layer2Info);
        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);

        // simulate the roaming by updating bssid.
        forceLayer2Roaming();

        currentTime = System.currentTimeMillis();
        reset(mIpMemoryStore);
        reset(mCb);
        if (!expectRoaming) {
            assertIpMemoryNeverStoreNetworkAttributes();
            return;
        }
        // check DHCPREQUEST broadcast sent to renew IP address.
        final DhcpPacket packet = getNextDhcpPacket();
        assertDhcpRequestForReacquire(packet);

        final ByteBuffer packetBuffer = shouldReplyNakOnRoam
                ? buildDhcpNakPacket(packet, "request IP on a wrong subnet")
                : buildDhcpAckPacket(packet,
                        hasMismatchedIpAddress ? CLIENT_ADDR_NEW : CLIENT_ADDR,
                        TEST_LEASE_DURATION_S, (short) TEST_DEFAULT_MTU,
                        false /* rapidCommit */, null /* captivePortalApiUrl */);
        mPacketReader.sendResponse(packetBuffer);
        HandlerUtils.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);

        if (shouldReplyNakOnRoam) {
            ArgumentCaptor<ReachabilityLossInfoParcelable> lossInfoCaptor =
                    ArgumentCaptor.forClass(ReachabilityLossInfoParcelable.class);
            verify(mCb, timeout(TEST_TIMEOUT_MS)).onReachabilityFailure(lossInfoCaptor.capture());
            assertEquals(ReachabilityLossReason.ROAM, lossInfoCaptor.getValue().reason);

            // IPv4 address will be still deleted when DhcpClient state machine exits from
            // DhcpHaveLeaseState, a following onProvisioningFailure will be thrown then.
            // Also check DhcpClient won't send any DHCPDISCOVER packet.
            verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningFailure(any());
            assertNull(getNextDhcpPacket(TEST_TIMEOUT_MS));
            verify(mCb, never()).onNewDhcpResults(any());
        } else if (hasMismatchedIpAddress) {
            ArgumentCaptor<DhcpResultsParcelable> resultsCaptor =
                    ArgumentCaptor.forClass(DhcpResultsParcelable.class);
            verify(mCb, timeout(TEST_TIMEOUT_MS)).onNewDhcpResults(resultsCaptor.capture());
            final DhcpResultsParcelable lease = resultsCaptor.getValue();
            assertNull(lease);

            // DhcpClient rolls back to StoppedState instead of INIT state after calling
            // notifyFailure, DHCPDISCOVER should not be sent out.
            assertNull(getNextDhcpPacket(TEST_TIMEOUT_MS));
        } else {
            assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime,
                    TEST_DEFAULT_MTU);
        }
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpRoaming() throws Exception {
        doDhcpRoamingTest(false /* hasMismatchedIpAddress */, "\"0001docomo\"" /* display name */,
                MacAddress.fromString(TEST_DEFAULT_BSSID), true /* expectRoaming */,
                false /* shouldReplyNakOnRoam */);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpRoaming_invalidBssid() throws Exception {
        doDhcpRoamingTest(false /* hasMismatchedIpAddress */, "\"0001docomo\"" /* display name */,
                MacAddress.fromString(TEST_DHCP_ROAM_BSSID), false /* expectRoaming */,
                false/* shouldReplyNakOnRoam */);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpRoaming_nullBssid() throws Exception {
        doDhcpRoamingTest(false /* hasMismatchedIpAddress */, "\"0001docomo\"" /* display name */,
                null /* BSSID */, false /* expectRoaming */, false /* shouldReplyNakOnRoam */);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpRoaming_invalidDisplayName() throws Exception {
        doDhcpRoamingTest(false /* hasMismatchedIpAddress */, "\"test-ssid\"" /* display name */,
                MacAddress.fromString(TEST_DEFAULT_BSSID), false /* expectRoaming */,
                false /* shouldReplyNakOnRoam */);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpRoaming_mismatchedLeasedIpAddress() throws Exception {
        doDhcpRoamingTest(true /* hasMismatchedIpAddress */, "\"0001docomo\"" /* display name */,
                MacAddress.fromString(TEST_DEFAULT_BSSID), true /* expectRoaming */,
                false /* shouldReplyNakOnRoam */);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDhcpRoaming_failureLeaseOnNak() throws Exception {
        doDhcpRoamingTest(false /* hasMismatchedIpAddress */, "\"0001docomo\"" /* display name */,
                MacAddress.fromString(TEST_DEFAULT_BSSID), true /* expectRoaming */,
                true /* shouldReplyNakOnRoam */);
    }

    private LinkProperties performDualStackProvisioning() throws Exception {
        final Inet6Address dnsServer = ipv6Addr(IPV6_OFF_LINK_DNS_SERVER);
        final ByteBuffer ra = buildRaPacket(TEST_IPV6_PREFIX, IPV6_OFF_LINK_DNS_SERVER,
                3600 /* validLifetime */, 1800 /* preferredLifetime */, 3600 /* dnsLifetime */,
                true /* shouldIncludeSlla */);

        return performDualStackProvisioning(ra, dnsServer);
    }

    private LinkProperties performDualStackProvisioning(final ByteBuffer ra,
            final InetAddress dnsServer) throws Exception {
        final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>();

        // Start IPv4 provisioning first and wait IPv4 provisioning to succeed, and then start
        // IPv6 provisioning, which is more realistic and avoid the flaky case of both IPv4 and
        // IPv6 provisioning complete at the same time.
        handleDhcpPackets(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
                true /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU, null /* serverSentUrl */);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(any());

        waitForRouterSolicitation();
        mPacketReader.sendResponse(ra);

        // Wait until we see both success IPv4 and IPv6 provisioning, then there would be 4
        // addresses in LinkProperties, they are IPv4 address, IPv6 link-local address, stable
        // privacy address and privacy address.
        verify(mCb, timeout(TEST_TIMEOUT_MS).atLeastOnce()).onLinkPropertiesChange(argThat(x -> {
            if (!x.isIpv4Provisioned() || !x.isIpv6Provisioned()) return false;
            if (x.getLinkAddresses().size() != 4) return false;
            lpFuture.complete(x);
            return true;
        }));

        final LinkProperties lp = lpFuture.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        assertNotNull(lp);
        assertTrue(lp.getDnsServers().contains(dnsServer));
        assertTrue(lp.getDnsServers().contains(SERVER_ADDR));
        assertHasAddressThat("link-local address", lp, x -> x.getAddress().isLinkLocalAddress());
        assertHasAddressThat("privacy address", lp, this::isPrivacyAddress);
        assertHasAddressThat("stable privacy address", lp, this::isStablePrivacyAddress);

        return lp;
    }

    private LinkProperties doDualStackProvisioning() throws Exception {
        final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIpReachabilityMonitor()
                .build();

        // Enable rapid commit to accelerate DHCP handshake to shorten test duration,
        // not strictly necessary.
        setDhcpFeatures(true /* isRapidCommitEnabled */, false /* isDhcpIpConflictDetectEnabled */);
        // Both signature and root tests can use this function to do dual-stack provisioning.
        if (useNetworkStackSignature()) {
            mIpc.startProvisioning(config);
        } else {
            mIIpClient.startProvisioning(config.toStableParcelable());
        }

        return performDualStackProvisioning();
    }

    private boolean hasRouteTo(@NonNull final LinkProperties lp, @NonNull final String prefix) {
        return hasRouteTo(lp, prefix, RTN_UNICAST);
    }

    private boolean hasRouteTo(@NonNull final LinkProperties lp, @NonNull final String prefix,
            int type) {
        for (RouteInfo r : lp.getRoutes()) {
            if (r.getDestination().equals(new IpPrefix(prefix))) return r.getType() == type;
        }
        return false;
    }

    private boolean hasIpv6AddressPrefixedWith(@NonNull final LinkProperties lp,
            @NonNull final IpPrefix prefix) {
        for (LinkAddress la : lp.getLinkAddresses()) {
            final InetAddress addr = la.getAddress();
            if ((addr instanceof Inet6Address) && !addr.isLinkLocalAddress()) {
                if (prefix.contains(addr)) return true;
            }
        }
        return false;
    }

    @Test
    @SignatureRequiredTest(reason = "Out of SLO flakiness")
    public void testIgnoreIpv6ProvisioningLoss_disableAcceptRaDefrtr() throws Exception {
        LinkProperties lp = doDualStackProvisioning();
        Log.d(TAG, "current LinkProperties: " + lp);

        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.
        sendRouterAdvertisementWithZeroRouterLifetime();
        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;
                }));
        lp = lpFuture.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        Log.d(TAG, "After receiving RA with 0 router lifetime, LinkProperties: " + lp);
        assertNotNull(lp);
        assertEquals(lp.getAddresses().get(0), CLIENT_ADDR);
        assertEquals(lp.getDnsServers().get(0), SERVER_ADDR);
        assertTrue(hasRouteTo(lp, IPV6_LINK_LOCAL_PREFIX)); // fe80::/64
        assertTrue(hasRouteTo(lp, IPV4_TEST_SUBNET_PREFIX)); // IPv4 directly-connected route
        assertTrue(hasRouteTo(lp, IPV4_ANY_ADDRESS_PREFIX)); // IPv4 default route
        assertTrue(lp.getAddresses().get(1).isLinkLocalAddress());

        clearInvocations(mCb);

        // Wait for RS after IPv6 stack has been restarted and reply with a normal RA to verify
        // that device gains the IPv6 provisioning without default route and off-link DNS server.
        sendBasicRouterAdvertisement(true /* waitForRs */);
        verify(mCb, timeout(TEST_TIMEOUT_MS).atLeastOnce()).onLinkPropertiesChange(argThat(
                x -> x.hasGlobalIpv6Address()
                        // IPv4, IPv6 link local, privacy and stable privacy
                        && x.getLinkAddresses().size() == 4
                        && !x.hasIpv6DefaultRoute()
                        && x.getDnsServers().size() == 1
                        && x.getDnsServers().get(0).equals(SERVER_ADDR)));
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDualStackProvisioning() throws Exception {
        doDualStackProvisioning();

        verify(mCb, never()).onProvisioningFailure(any());
    }

    private DhcpPacket verifyDhcpPacketRequestsIPv6OnlyPreferredOption(
            Class<? extends DhcpPacket> packetType) throws Exception {
        final DhcpPacket packet = getNextDhcpPacket();
        assertTrue(packetType.isInstance(packet));
        assertTrue(packet.hasRequestedParam(DHCP_IPV6_ONLY_PREFERRED));
        return packet;
    }

    private void doIPv6OnlyPreferredOptionTest(final Integer ipv6OnlyWaitTime,
            final Inet4Address clientAddress) throws Exception {
        final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIpReachabilityMonitor()
                .build();
        setDhcpFeatures(false /* isRapidCommitEnabled */,
                false /* isDhcpIpConflictDetectEnabled */);
        startIpClientProvisioning(config);

        final DhcpPacket packet =
                verifyDhcpPacketRequestsIPv6OnlyPreferredOption(DhcpDiscoverPacket.class);

        // Respond DHCPOFFER with IPv6-Only preferred option and offered address.
        mPacketReader.sendResponse(buildDhcpOfferPacket(packet, clientAddress,
                TEST_LEASE_DURATION_S, (short) TEST_DEFAULT_MTU, null /* captivePortalUrl */,
                ipv6OnlyWaitTime, null /* domainName */, null /* domainSearchList */));
    }

    private void doDiscoverIPv6OnlyPreferredOptionTest(final int optionSecs,
            final long expectedWaitSecs) throws Exception {
        doIPv6OnlyPreferredOptionTest(optionSecs, CLIENT_ADDR);
        final OnAlarmListener alarm = expectAlarmSet(null /* inOrder */, "TIMEOUT",
                expectedWaitSecs, mDependencies.mDhcpClient.getHandler());
        mDependencies.mDhcpClient.getHandler().post(() -> alarm.onAlarm());
        // Implicitly check that the client never sent a DHCPREQUEST to request the offered address.
        verifyDhcpPacketRequestsIPv6OnlyPreferredOption(DhcpDiscoverPacket.class);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDiscoverIPv6OnlyPreferredOption() throws Exception {
        doDiscoverIPv6OnlyPreferredOptionTest(TEST_IPV6_ONLY_WAIT_S, TEST_IPV6_ONLY_WAIT_S);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDiscoverIPv6OnlyPreferredOption_LowerIPv6OnlyWait() throws Exception {
        doDiscoverIPv6OnlyPreferredOptionTest(TEST_LOWER_IPV6_ONLY_WAIT_S,
                TEST_LOWER_IPV6_ONLY_WAIT_S);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDiscoverIPv6OnlyPreferredOption_ZeroIPv6OnlyWait() throws Exception {
        doDiscoverIPv6OnlyPreferredOptionTest(TEST_ZERO_IPV6_ONLY_WAIT_S,
                TEST_LOWER_IPV6_ONLY_WAIT_S);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDiscoverIPv6OnlyPreferredOption_MaxIPv6OnlyWait() throws Exception {
        doDiscoverIPv6OnlyPreferredOptionTest((int) TEST_MAX_IPV6_ONLY_WAIT_S, 0xffffffffL);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDiscoverIPv6OnlyPreferredOption_ZeroIPv6OnlyWaitWithOfferedAnyAddress()
            throws Exception {
        doIPv6OnlyPreferredOptionTest(TEST_ZERO_IPV6_ONLY_WAIT_S, IPV4_ADDR_ANY);

        final OnAlarmListener alarm = expectAlarmSet(null /* inOrder */, "TIMEOUT", 300,
                mDependencies.mDhcpClient.getHandler());
        mDependencies.mDhcpClient.getHandler().post(() -> alarm.onAlarm());

        verifyDhcpPacketRequestsIPv6OnlyPreferredOption(DhcpDiscoverPacket.class);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDiscoverIPv6OnlyPreferredOption_enabledPreconnection() throws Exception {
        final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIpReachabilityMonitor()
                .withPreconnection()
                .build();

        setDhcpFeatures(true /* isRapidCommitEnabled */, false /* isDhcpIpConflictDetectEnabled */);
        startIpClientProvisioning(config);

        final DhcpPacket packet = assertDiscoverPacketOnPreconnectionStart();
        verify(mCb).setNeighborDiscoveryOffload(true);

        // Force IpClient transition to RunningState from PreconnectionState.
        mIpc.notifyPreconnectionComplete(true /* success */);
        HandlerUtils.waitForIdle(mDependencies.mDhcpClient.getHandler(), TEST_TIMEOUT_MS);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true);

        // DHCP server SHOULD NOT honor the Rapid-Commit option if the response would
        // contain the IPv6-only Preferred option to the client, instead respond with
        // a DHCPOFFER.
        mPacketReader.sendResponse(buildDhcpOfferPacket(packet, CLIENT_ADDR, TEST_LEASE_DURATION_S,
                (short) TEST_DEFAULT_MTU, null /* captivePortalUrl */, TEST_IPV6_ONLY_WAIT_S,
                null /* domainName */, null /* domainSearchList */));

        final OnAlarmListener alarm = expectAlarmSet(null /* inOrder */, "TIMEOUT", 1800,
                mDependencies.mDhcpClient.getHandler());
        mDependencies.mDhcpClient.getHandler().post(() -> alarm.onAlarm());

        verifyDhcpPacketRequestsIPv6OnlyPreferredOption(DhcpDiscoverPacket.class);
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testDiscoverIPv6OnlyPreferredOption_NoIPv6OnlyPreferredOption() throws Exception {
        doIPv6OnlyPreferredOptionTest(null /* ipv6OnlyWaitTime */, CLIENT_ADDR);

        // The IPv6-only Preferred option SHOULD be included in the Parameter Request List option
        // in DHCPREQUEST messages after receiving a DHCPOFFER without this option.
        verifyDhcpPacketRequestsIPv6OnlyPreferredOption(DhcpRequestPacket.class);
    }

    private void setUpRetrievedNetworkAttributesForInitRebootState() {
        final NetworkAttributes na = new NetworkAttributes.Builder()
                .setAssignedV4Address(CLIENT_ADDR)
                .setAssignedV4AddressExpiry(Long.MAX_VALUE) // lease is always valid
                .setMtu(new Integer(TEST_DEFAULT_MTU))
                .setCluster(TEST_CLUSTER)
                .setDnsAddresses(Collections.singletonList(SERVER_ADDR))
                .build();
        storeNetworkAttributes(TEST_L2KEY, na);
    }

    private void startFromInitRebootStateWithIPv6OnlyPreferredOption(final Integer ipv6OnlyWaitTime,
            final long expectedWaitSecs) throws Exception {
        setUpRetrievedNetworkAttributesForInitRebootState();

        final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIpReachabilityMonitor()
                .withLayer2Information(new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
                          MacAddress.fromString(TEST_DEFAULT_BSSID)))
                .build();

        setDhcpFeatures(false /* isRapidCommitEnabled */,
                false /* isDhcpIpConflictDetectEnabled */);
        startIpClientProvisioning(config);

        final DhcpPacket packet =
                verifyDhcpPacketRequestsIPv6OnlyPreferredOption(DhcpRequestPacket.class);

        // Respond DHCPACK with IPv6-Only preferred option.
        mPacketReader.sendResponse(buildDhcpAckPacket(packet, CLIENT_ADDR,
                TEST_LEASE_DURATION_S, (short) TEST_DEFAULT_MTU, false /* rapidcommit */,
                null /* captivePortalUrl */, ipv6OnlyWaitTime, null /* domainName */,
                null /* domainSearchList */));

        if (ipv6OnlyWaitTime != null) {
            expectAlarmSet(null /* inOrder */, "TIMEOUT", expectedWaitSecs,
                    mDependencies.mDhcpClient.getHandler());
        }
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testRequestIPv6OnlyPreferredOption() throws Exception {
        startFromInitRebootStateWithIPv6OnlyPreferredOption(TEST_IPV6_ONLY_WAIT_S,
                TEST_IPV6_ONLY_WAIT_S);

        // Client transits to IPv6OnlyPreferredState from INIT-REBOOT state when receiving valid
        // IPv6-Only preferred option(default value) in the DHCPACK packet.
        assertIpMemoryNeverStoreNetworkAttributes();
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testRequestIPv6OnlyPreferredOption_LowerIPv6OnlyWait() throws Exception {
        startFromInitRebootStateWithIPv6OnlyPreferredOption(TEST_LOWER_IPV6_ONLY_WAIT_S,
                TEST_LOWER_IPV6_ONLY_WAIT_S);

        // Client transits to IPv6OnlyPreferredState from INIT-REBOOT state when receiving valid
        // IPv6-Only preferred option(less than MIN_V6ONLY_WAIT_MS) in the DHCPACK packet.
        assertIpMemoryNeverStoreNetworkAttributes();
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testRequestIPv6OnlyPreferredOption_ZeroIPv6OnlyWait() throws Exception {
        startFromInitRebootStateWithIPv6OnlyPreferredOption(TEST_ZERO_IPV6_ONLY_WAIT_S,
                TEST_LOWER_IPV6_ONLY_WAIT_S);

        // Client transits to IPv6OnlyPreferredState from INIT-REBOOT state when receiving valid
        // IPv6-Only preferred option(0) in the DHCPACK packet.
        assertIpMemoryNeverStoreNetworkAttributes();
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testRequestIPv6OnlyPreferredOption_MaxIPv6OnlyWait() throws Exception {
        startFromInitRebootStateWithIPv6OnlyPreferredOption((int) TEST_MAX_IPV6_ONLY_WAIT_S,
                0xffffffffL);

        // Client transits to IPv6OnlyPreferredState from INIT-REBOOT state when receiving valid
        // IPv6-Only preferred option(MAX_UNSIGNED_INTEGER: 0xFFFFFFFF) in the DHCPACK packet.
        assertIpMemoryNeverStoreNetworkAttributes();
    }

    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
    public void testRequestIPv6OnlyPreferredOption_NoIPv6OnlyPreferredOption() throws Exception {
        final long currentTime = System.currentTimeMillis();
        startFromInitRebootStateWithIPv6OnlyPreferredOption(null /* ipv6OnlyWaitTime */,
                0 /* expectedWaitSecs */);

        // Client processes DHCPACK packet normally and transits to the ConfiguringInterfaceState
        // due to the null V6ONLY_WAIT.
        assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
    }

    private static int getNumOpenFds() {
        return new File("/proc/" + Os.getpid() + "/fd").listFiles().length;
    }

    private void shutdownAndRecreateIpClient() throws Exception {
        clearInvocations(mCb);
        mIpc.shutdown();
        awaitIpClientShutdown();
        mIpc = makeIpClient();
    }

    @Test @SignatureRequiredTest(reason = "Only counts FDs from the current process. TODO: fix")
    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();
        shutdownAndRecreateIpClient();

        // Unfortunately we cannot use a large number of iterations as it would make the test run
        // too slowly. On crosshatch-eng each iteration takes ~250ms.
        final int iterations = 10;
        final int before = getNumOpenFds();
        for (int i = 0; i < iterations; i++) {
            doDualStackProvisioning();
            shutdownAndRecreateIpClient();
            // The last time this loop runs, mIpc will be shut down in tearDown.
        }
        final int after = getNumOpenFds();

        // Check that the number of open fds is the same as before, within some tolerance (e.g.,
        // garbage collection or other cleanups might have caused an fd to be closed). This
        // shouldn't make leak detection much less reliable, since it's likely that any leak would
        // at least leak one FD per loop.
        final int tolerance = 4;
        assertTrue(
                "FD leak detected after " + iterations + " iterations: expected "
                        + before + " +/- " + tolerance + " fds, found " + after,
                Math.abs(after - before) <= tolerance);
    }

    // TODO: delete when DhcpOption is @JavaOnlyImmutable.
    private static DhcpOption makeDhcpOption(final byte type, final byte[] value) {
        final DhcpOption opt = new DhcpOption();
        opt.type = type;
        opt.value = value;
        return opt;
    }

    private static final List<DhcpOption> TEST_OEM_DHCP_OPTIONS = Arrays.asList(
            // DHCP_USER_CLASS
            makeDhcpOption((byte) 77, TEST_OEM_USER_CLASS_INFO),
            // DHCP_VENDOR_CLASS_ID
            makeDhcpOption((byte) 60, TEST_OEM_VENDOR_ID.getBytes())
    );

    private DhcpPacket doCustomizedDhcpOptionsTest(final List<DhcpOption> options,
             final ScanResultInfo info) throws Exception {
        ProvisioningConfiguration.Builder prov = new ProvisioningConfiguration.Builder()
                .withoutIpReachabilityMonitor()
                .withLayer2Information(new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
                        MacAddress.fromString(TEST_DEFAULT_BSSID)))
                .withScanResultInfo(info)
                .withDhcpOptions(options)
                .withoutIPv6();

        setDhcpFeatures(false /* isRapidCommitEnabled */,
                false /* isDhcpIpConflictDetectEnabled */);

        startIpClientProvisioning(prov.build());
        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true);
        verify(mCb, never()).onProvisioningFailure(any());

        return getNextDhcpPacket();
    }

    @Test
    public void testDiscoverCustomizedDhcpOptions() throws Exception {
        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
                TEST_VENDOR_SPECIFIC_IE_TYPE);
        final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info);

        assertTrue(packet instanceof DhcpDiscoverPacket);
        assertEquals(packet.mVendorId, TEST_OEM_VENDOR_ID);
        assertArrayEquals(packet.mUserClass, TEST_OEM_USER_CLASS_INFO);
    }

    @Test
    public void testDiscoverCustomizedDhcpOptions_nullDhcpOptions() throws Exception {
        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
                TEST_VENDOR_SPECIFIC_IE_TYPE);
        final DhcpPacket packet = doCustomizedDhcpOptionsTest(null /* options */, info);

        assertTrue(packet instanceof DhcpDiscoverPacket);
        assertEquals(packet.mVendorId, new String("android-dhcp-" + Build.VERSION.RELEASE));
        assertNull(packet.mUserClass);
    }

    @Test
    public void testDiscoverCustomizedDhcpOptions_nullScanResultInfo() throws Exception {
        final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS,
                null /* scanResultInfo */);

        assertTrue(packet instanceof DhcpDiscoverPacket);
        assertEquals(packet.mVendorId, new String("android-dhcp-" + Build.VERSION.RELEASE));
        assertNull(packet.mUserClass);
    }

    @Test
    public void testDiscoverCustomizedDhcpOptions_disallowedOui() throws Exception {
        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID,
                new byte[]{ 0x00, 0x11, 0x22} /* oui */, TEST_VENDOR_SPECIFIC_IE_TYPE);
        final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info);

        assertTrue(packet instanceof DhcpDiscoverPacket);
        assertEquals(packet.mVendorId, new String("android-dhcp-" + Build.VERSION.RELEASE));
        assertNull(packet.mUserClass);
    }

    @Test
    public void testDiscoverCustomizedDhcpOptions_invalidIeId() throws Exception {
        final ScanResultInfo info = makeScanResultInfo(0xde /* vendor-specific IE */, TEST_OEM_OUI,
                TEST_VENDOR_SPECIFIC_IE_TYPE);
        final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info);

        assertTrue(packet instanceof DhcpDiscoverPacket);
        assertEquals(packet.mVendorId, new String("android-dhcp-" + Build.VERSION.RELEASE));
        assertNull(packet.mUserClass);
    }

    @Test
    public void testDiscoverCustomizedDhcpOptions_invalidVendorSpecificType() throws Exception {
        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
                (byte) 0x10 /* vendor-specific IE type */);
        final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info);

        assertTrue(packet instanceof DhcpDiscoverPacket);
        assertEquals(packet.mVendorId, new String("android-dhcp-" + Build.VERSION.RELEASE));
        assertNull(packet.mUserClass);
    }

    @Test
    public void testDiscoverCustomizedDhcpOptions_legacyVendorSpecificType() throws Exception {
        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
                 LEGACY_TEST_VENDOR_SPECIFIC_IE_TYPE);
        final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info);

        assertTrue(packet instanceof DhcpDiscoverPacket);
        assertEquals(packet.mVendorId, new String("android-dhcp-" + Build.VERSION.RELEASE));
        assertNull(packet.mUserClass);
    }

    @Test
    public void testDisoverCustomizedDhcpOptions_disallowedOption() throws Exception {
        final List<DhcpOption> options = Arrays.asList(
                makeDhcpOption((byte) 60, TEST_OEM_VENDOR_ID.getBytes()),
                makeDhcpOption((byte) 77, TEST_OEM_USER_CLASS_INFO),
                // Option 26: MTU
                makeDhcpOption((byte) 26, HexDump.toByteArray(TEST_DEFAULT_MTU)));
        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
                TEST_VENDOR_SPECIFIC_IE_TYPE);
        final DhcpPacket packet = doCustomizedDhcpOptionsTest(options, info);

        assertTrue(packet instanceof DhcpDiscoverPacket);
        assertEquals(packet.mVendorId, TEST_OEM_VENDOR_ID);
        assertArrayEquals(packet.mUserClass, TEST_OEM_USER_CLASS_INFO);
        assertNull(packet.mMtu);
    }

    @Test
    public void testDiscoverCustomizedDhcpOptions_disallowedParamRequestOption() throws Exception {
        final List<DhcpOption> options = Arrays.asList(
                makeDhcpOption((byte) 60, TEST_OEM_VENDOR_ID.getBytes()),
                makeDhcpOption((byte) 77, TEST_OEM_USER_CLASS_INFO),
                // NTP_SERVER
                makeDhcpOption((byte) 42, null));
        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
                TEST_VENDOR_SPECIFIC_IE_TYPE);
        final DhcpPacket packet = doCustomizedDhcpOptionsTest(options, info);

        assertTrue(packet instanceof DhcpDiscoverPacket);
        assertEquals(packet.mVendorId, TEST_OEM_VENDOR_ID);
        assertArrayEquals(packet.mUserClass, TEST_OEM_USER_CLASS_INFO);
        assertFalse(packet.hasRequestedParam((byte) 42 /* NTP_SERVER */));
    }

    @Test
    public void testDiscoverCustomizedDhcpOptions_ParameterRequestListOnly() throws Exception {
        final List<DhcpOption> options = Arrays.asList(
                // DHCP_USER_CLASS
                makeDhcpOption((byte) 77, null));
        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
                TEST_VENDOR_SPECIFIC_IE_TYPE);
        final DhcpPacket packet = doCustomizedDhcpOptionsTest(options, info);

        assertTrue(packet instanceof DhcpDiscoverPacket);
        assertTrue(packet.hasRequestedParam((byte) 77 /* DHCP_USER_CLASS */));
        assertNull(packet.mUserClass);
    }

    @Test
    public void testRequestCustomizedDhcpOptions() throws Exception {
        setUpRetrievedNetworkAttributesForInitRebootState();

        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
                TEST_VENDOR_SPECIFIC_IE_TYPE);
        final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info);

        assertTrue(packet instanceof DhcpRequestPacket);
        assertEquals(packet.mVendorId, TEST_OEM_VENDOR_ID);
        assertArrayEquals(packet.mUserClass, TEST_OEM_USER_CLASS_INFO);
    }

    @Test
    public void testRequestCustomizedDhcpOptions_nullDhcpOptions() throws Exception {
        setUpRetrievedNetworkAttributesForInitRebootState();

        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
                TEST_VENDOR_SPECIFIC_IE_TYPE);
        final DhcpPacket packet = doCustomizedDhcpOptionsTest(null /* options */, info);

        assertTrue(packet instanceof DhcpRequestPacket);
        assertEquals(packet.mVendorId, new String("android-dhcp-" + Build.VERSION.RELEASE));
        assertNull(packet.mUserClass);
    }

    @Test
    public void testRequestCustomizedDhcpOptions_nullScanResultInfo() throws Exception {
        setUpRetrievedNetworkAttributesForInitRebootState();

        final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS,
                null /* scanResultInfo */);

        assertTrue(packet instanceof DhcpRequestPacket);
        assertEquals(packet.mVendorId, new String("android-dhcp-" + Build.VERSION.RELEASE));
        assertNull(packet.mUserClass);
    }

    @Test
    public void testRequestCustomizedDhcpOptions_disallowedOui() throws Exception {
        setUpRetrievedNetworkAttributesForInitRebootState();

        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID,
                new byte[]{ 0x00, 0x11, 0x22} /* oui */, TEST_VENDOR_SPECIFIC_IE_TYPE);
        final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info);

        assertTrue(packet instanceof DhcpRequestPacket);
        assertEquals(packet.mVendorId, new String("android-dhcp-" + Build.VERSION.RELEASE));
        assertNull(packet.mUserClass);
    }

    @Test
    public void testRequestCustomizedDhcpOptions_invalidIeId() throws Exception {
        setUpRetrievedNetworkAttributesForInitRebootState();

        final ScanResultInfo info = makeScanResultInfo(0xde /* vendor-specific IE */, TEST_OEM_OUI,
                TEST_VENDOR_SPECIFIC_IE_TYPE);
        final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info);

        assertTrue(packet instanceof DhcpRequestPacket);
        assertEquals(packet.mVendorId, new String("android-dhcp-" + Build.VERSION.RELEASE));
        assertNull(packet.mUserClass);
    }

    @Test
    public void testRequestCustomizedDhcpOptions_invalidVendorSpecificType() throws Exception {
        setUpRetrievedNetworkAttributesForInitRebootState();

        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
                (byte) 0x20 /* vendor-specific IE type */);
        final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info);

        assertTrue(packet instanceof DhcpRequestPacket);
        assertEquals(packet.mVendorId, new String("android-dhcp-" + Build.VERSION.RELEASE));
        assertNull(packet.mUserClass);
    }

    @Test
    public void testRequestCustomizedDhcpOptions_legacyVendorSpecificType() throws Exception {
        setUpRetrievedNetworkAttributesForInitRebootState();

        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
                LEGACY_TEST_VENDOR_SPECIFIC_IE_TYPE);
        final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info);

        assertTrue(packet instanceof DhcpRequestPacket);
        assertEquals(packet.mVendorId, new String("android-dhcp-" + Build.VERSION.RELEASE));
        assertNull(packet.mUserClass);
    }

    @Test
    public void testRequestCustomizedDhcpOptions_disallowedOption() throws Exception {
        setUpRetrievedNetworkAttributesForInitRebootState();

        final List<DhcpOption> options = Arrays.asList(
                makeDhcpOption((byte) 60, TEST_OEM_VENDOR_ID.getBytes()),
                makeDhcpOption((byte) 77, TEST_OEM_USER_CLASS_INFO),
                // Option 26: MTU
                makeDhcpOption((byte) 26, HexDump.toByteArray(TEST_DEFAULT_MTU)));
        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
                TEST_VENDOR_SPECIFIC_IE_TYPE);
        final DhcpPacket packet = doCustomizedDhcpOptionsTest(options, info);

        assertTrue(packet instanceof DhcpRequestPacket);
        assertEquals(packet.mVendorId, TEST_OEM_VENDOR_ID);
        assertArrayEquals(packet.mUserClass, TEST_OEM_USER_CLASS_INFO);
        assertNull(packet.mMtu);
    }

    @Test
    public void testRequestCustomizedDhcpOptions_disallowedParamRequestOption() throws Exception {
        setUpRetrievedNetworkAttributesForInitRebootState();

        final List<DhcpOption> options = Arrays.asList(
                makeDhcpOption((byte) 60, TEST_OEM_VENDOR_ID.getBytes()),
                makeDhcpOption((byte) 77, TEST_OEM_USER_CLASS_INFO),
                // NTP_SERVER
                makeDhcpOption((byte) 42, null));
        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
                TEST_VENDOR_SPECIFIC_IE_TYPE);
        final DhcpPacket packet = doCustomizedDhcpOptionsTest(options, info);

        assertTrue(packet instanceof DhcpRequestPacket);
        assertEquals(packet.mVendorId, TEST_OEM_VENDOR_ID);
        assertArrayEquals(packet.mUserClass, TEST_OEM_USER_CLASS_INFO);
        assertFalse(packet.hasRequestedParam((byte) 42 /* NTP_SERVER */));
    }

    @Test
    public void testRequestCustomizedDhcpOptions_ParameterRequestListOnly() throws Exception {
        setUpRetrievedNetworkAttributesForInitRebootState();

        final List<DhcpOption> options = Arrays.asList(
                // DHCP_USER_CLASS
                makeDhcpOption((byte) 77, null));
        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
                TEST_VENDOR_SPECIFIC_IE_TYPE);
        final DhcpPacket packet = doCustomizedDhcpOptionsTest(options, info);

        assertTrue(packet instanceof DhcpRequestPacket);
        assertTrue(packet.hasRequestedParam((byte) 77 /* DHCP_USER_CLASS */));
        assertNull(packet.mUserClass);
    }

    private void assertGratuitousNa(final NeighborAdvertisement na) throws Exception {
        final MacAddress etherMulticast =
                NetworkStackUtils.ipv6MulticastToEthernetMulticast(IPV6_ADDR_ALL_ROUTERS_MULTICAST);
        final LinkAddress target = new LinkAddress(na.naHdr.target, 64);

        assertEquals(etherMulticast, na.ethHdr.dstMac);
        assertEquals(ETH_P_IPV6, na.ethHdr.etherType);
        assertEquals(IPPROTO_ICMPV6, na.ipv6Hdr.nextHeader);
        assertEquals(0xff, na.ipv6Hdr.hopLimit);
        assertTrue(na.ipv6Hdr.srcIp.isLinkLocalAddress());
        assertEquals(IPV6_ADDR_ALL_ROUTERS_MULTICAST, na.ipv6Hdr.dstIp);
        assertEquals(ICMPV6_NEIGHBOR_ADVERTISEMENT, na.icmpv6Hdr.type);
        assertEquals(0, na.icmpv6Hdr.code);
        assertEquals(0, na.naHdr.flags);
        assertTrue(target.isGlobalPreferred());
    }

    private void assertMulticastNsFromIpv6Gua(final NeighborSolicitation ns) throws Exception {
        final Inet6Address solicitedNodeMulticast =
                NetworkStackUtils.ipv6AddressToSolicitedNodeMulticast(ROUTER_LINK_LOCAL);
        final MacAddress etherMulticast =
                NetworkStackUtils.ipv6MulticastToEthernetMulticast(solicitedNodeMulticast);

        assertEquals(etherMulticast, ns.ethHdr.dstMac);
        assertEquals(ETH_P_IPV6, ns.ethHdr.etherType);
        assertEquals(IPPROTO_ICMPV6, ns.ipv6Hdr.nextHeader);
        assertEquals(0xff, ns.ipv6Hdr.hopLimit);

        final LinkAddress srcIp = new LinkAddress(ns.ipv6Hdr.srcIp.getHostAddress() + "/64");
        assertTrue(srcIp.isGlobalPreferred());
        assertEquals(solicitedNodeMulticast, ns.ipv6Hdr.dstIp);
        assertEquals(ICMPV6_NEIGHBOR_SOLICITATION, ns.icmpv6Hdr.type);
        assertEquals(0, ns.icmpv6Hdr.code);
        assertEquals(ROUTER_LINK_LOCAL, ns.nsHdr.target);
    }

    @Test
    public void testGratuitousNaForNewGlobalUnicastAddresses() throws Exception {
        final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIpReachabilityMonitor()
                .withoutIPv4()
                .build();

        startIpClientProvisioning(config);

        doIpv6OnlyProvisioning();

        final List<NeighborAdvertisement> naList = new ArrayList<>();
        NeighborAdvertisement packet;
        while ((packet = getNextNeighborAdvertisement()) != null) {
            assertGratuitousNa(packet);
            naList.add(packet);
        }
        assertEquals(2, naList.size()); // privacy address and stable privacy address
    }

    private void startGratuitousArpAndNaAfterRoamingTest(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();
        if (!hasIpv6) prov.withoutIPv6();

        // Enable rapid commit to accelerate DHCP handshake to shorten test duration,
        // not strictly necessary.
        setDhcpFeatures(true /* isRapidCommitEnabled */,
                false /* isDhcpIpConflictDetectEnabled */);
        startIpClientProvisioning(prov.build());
    }

    private void waitForGratuitousArpAndNaPacket(final List<ArpPacket> arpList,
            final List<NeighborAdvertisement> naList) throws Exception {
        NeighborAdvertisement na;
        ArpPacket garp;
        do {
            na = getNextNeighborAdvertisement();
            if (na != null) {
                assertGratuitousNa(na);
                naList.add(na);
            }
            garp = getNextArpPacket(TEST_TIMEOUT_MS);
            if (garp != null) {
                assertGratuitousARP(garp);
                arpList.add(garp);
            }
        } while (na != null || garp != null);
    }

    @Test
    public void testGratuitousArpAndNaAfterRoaming() throws Exception {
        startGratuitousArpAndNaAfterRoamingTest(true /* hasIpv4 */, true /* hasIpv6 */);
        performDualStackProvisioning();
        forceLayer2Roaming();

        final List<ArpPacket> arpList = new ArrayList<>();
        final List<NeighborAdvertisement> naList = new ArrayList<>();
        waitForGratuitousArpAndNaPacket(arpList, naList);
        // 2 NAs sent due to RFC9131 implement and 2 NAs sent after roam
        assertEquals(4, naList.size()); // privacy address and stable privacy address
        assertEquals(1, arpList.size()); // IPv4 address
    }

    @Test
    public void testGratuitousArpAndNaAfterRoaming_IPv6OnlyNetwork() throws Exception {
        startGratuitousArpAndNaAfterRoamingTest(false /* hasIpv4 */, true /* hasIpv6 */);
        doIpv6OnlyProvisioning();
        forceLayer2Roaming();

        final List<ArpPacket> arpList = new ArrayList<>();
        final List<NeighborAdvertisement> naList = new ArrayList<>();
        waitForGratuitousArpAndNaPacket(arpList, naList);
        // 2 NAs sent due to RFC9131 implement and 2 NAs sent after roam
        assertEquals(4, naList.size());
        assertEquals(0, arpList.size());
    }

    @Test
    public void testGratuitousArpAndNaAfterRoaming_IPv4OnlyNetwork() throws Exception {
        startGratuitousArpAndNaAfterRoamingTest(true /* hasIpv4 */, false /* hasIpv6 */);

        // Start IPv4 provisioning and wait until entire provisioning completes.
        handleDhcpPackets(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
                true /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU, null /* serverSentUrl */);
        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
        forceLayer2Roaming();

        final List<ArpPacket> arpList = new ArrayList<>();
        final List<NeighborAdvertisement> naList = new ArrayList<>();
        waitForGratuitousArpAndNaPacket(arpList, naList);
        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);
        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 assertMulticastNeighborSolicitation(final NeighborSolicitation ns,
            final Inet6Address target) {
        final MacAddress etherMulticast =
                NetworkStackUtils.ipv6MulticastToEthernetMulticast(ns.ipv6Hdr.dstIp);
        assertEquals(etherMulticast, ns.ethHdr.dstMac);
        assertTrue(ns.ipv6Hdr.dstIp.isMulticastAddress());
        assertNeighborSolicitation(ns, target);
    }

    private NeighborSolicitation waitForUnicastNeighborSolicitation(final MacAddress dstMac,
            final Inet6Address dstIp, final Inet6Address targetIp) throws Exception {
        NeighborSolicitation ns;
        while ((ns = getNextNeighborSolicitation()) != null) {
            // Filter out the multicast NSes used for duplicate address detetction, the target
            // address is the global IPv6 address inside these NSes, and multicast NSes sent from
            // device's GUAs to force first-hop router to update the neighbor cache entry.
            if (ns.ipv6Hdr.srcIp.isLinkLocalAddress() && ns.nsHdr.target.isLinkLocalAddress()) {
                break;
            }
        }
        assertNotNull("No unicast Neighbor solicitation received on interface within timeout", ns);
        assertUnicastNeighborSolicitation(ns, dstMac, dstIp, targetIp);
        return ns;
    }

    private NeighborSolicitation waitForMulticastNeighborSolicitation(final Inet6Address targetIp)
            throws Exception {
        NeighborSolicitation ns;
        while ((ns = getNextNeighborSolicitation()) != null) {
            if (ns.ipv6Hdr.dstIp.isMulticastAddress() // Solicited-node multicast address
                    && ns.nsHdr.target.equals(targetIp)) {
                break;
            }
        }
        assertNotNull("No multicast Neighbor solicitation received on interface within timeout",
                ns);
        assertMulticastNeighborSolicitation(ns, targetIp);
        return ns;
    }

    private List<NeighborSolicitation> waitForMultipleNeighborSolicitations(int expectedNsCount)
            throws Exception {
        NeighborSolicitation ns;
        final List<NeighborSolicitation> nsList = new ArrayList<NeighborSolicitation>();
        while ((ns = getNextNeighborSolicitation()) != null) {
            // Filter out the multicast NSes used for duplicate address detection, the target
            // address is the global IPv6 address inside these NSes, and multicast NSes sent from
            // device's GUAs to force first-hop router to update the neighbor cache entry.
            if (ns.ipv6Hdr.srcIp.isLinkLocalAddress() && ns.nsHdr.target.isLinkLocalAddress()) {
                nsList.add(ns);
                if (nsList.size() == expectedNsCount) break;
            }
        }
        assertFalse(nsList.isEmpty());
        return nsList;
    }

    private NeighborSolicitation expectDadNeighborSolicitationForLinkLocal(boolean shouldDisableDad)
            throws Exception {
        final NeighborSolicitation ns = getNextNeighborSolicitation();
        if (!shouldDisableDad) {
            final Inet6Address solicitedNodeMulticast =
                    NetworkStackUtils.ipv6AddressToSolicitedNodeMulticast(ns.nsHdr.target);
            assertNotNull("No multicast NS received on interface within timeout", ns);
            assertEquals(IPV6_ADDR_ANY, ns.ipv6Hdr.srcIp);     // srcIp: ::/
            assertTrue(ns.ipv6Hdr.dstIp.isMulticastAddress()); // dstIp: solicited-node mcast
            assertTrue(ns.ipv6Hdr.dstIp.equals(solicitedNodeMulticast));
            assertTrue(ns.nsHdr.target.isLinkLocalAddress());  // targetIp: IPv6 LL address
        } else {
            assertNull(ns);
        }
        return ns;
    }

    // Override this function with disabled experiment flag by default, in order not to
    // affect those tests which are just related to basic IpReachabilityMonitor infra.
    private void prepareIpReachabilityMonitorTest() throws Exception {
        prepareIpReachabilityMonitorTest(false /* isMulticastResolicitEnabled */);
    }

    private void assertNotifyNeighborLost(Inet6Address targetIp, NudEventType eventType)
            throws Exception {
        // For root test suite, rely on the IIpClient aidl interface version constant defined in
        // {@link IpClientRootTest.BinderCbWrapper}; for privileged integration test suite that
        // requires signature permission, use the mocked aidl version defined in {@link setUpMocks},
        // which results in only new callbacks are verified. And add separate test cases to test the
        // legacy callbacks explicitly as well.
        assertNeighborReachabilityLoss(targetIp, eventType,
                useNetworkStackSignature()
                        ? IpClient.VERSION_ADDED_REACHABILITY_FAILURE
                        : mIIpClient.getInterfaceVersion());
    }

    private void assertNeighborReachabilityLoss(Inet6Address targetIp, NudEventType eventType,
            int targetAidlVersion) throws Exception {
        if (targetAidlVersion >= IpClient.VERSION_ADDED_REACHABILITY_FAILURE) {
            final ArgumentCaptor<ReachabilityLossInfoParcelable> lossInfoCaptor =
                    ArgumentCaptor.forClass(ReachabilityLossInfoParcelable.class);
            verify(mCb, timeout(TEST_TIMEOUT_MS)).onReachabilityFailure(lossInfoCaptor.capture());
            assertEquals(nudEventTypeToInt(eventType), lossInfoCaptor.getValue().reason);
            verify(mCb, never()).onReachabilityLost(any());
        } else {
            verify(mCb, timeout(TEST_TIMEOUT_MS)).onReachabilityLost(any());
            verify(mCb, never()).onReachabilityFailure(any());
        }
    }

    private void assertNeverNotifyNeighborLost() throws Exception {
        verify(mCb, never()).onReachabilityFailure(any());
        verify(mCb, never()).onReachabilityLost(any());
    }

    // If the UDP packet is sent to off-link address, the targetIp should be default gateway's IP,
    // otherwise, it should be the on-link DNS server address.
    private void expectAndRespondToMulticastNeighborSolicitation(final Inet6Address targetIp)
            throws Exception {
        final NeighborSolicitation ns = waitForMulticastNeighborSolicitation(targetIp);
        final MacAddress srcMac =
                targetIp.equals(ROUTER_LINK_LOCAL) ? ROUTER_MAC : ON_LINK_DNS_SERVER_MAC;
        final Inet6Address srcIp = targetIp.equals(ROUTER_LINK_LOCAL)
                ? ROUTER_LINK_LOCAL
                : ipv6Addr(IPV6_ON_LINK_DNS_SERVER);
        int flag = NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER | NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
        final ByteBuffer na = NeighborAdvertisement.build(srcMac,
                ns.ethHdr.srcMac /* dstMac */, srcIp,
                ns.ipv6Hdr.srcIp /* dstIp */, flag, targetIp);
        mPacketReader.sendResponse(na);
    }

    private void verifyRestoringNeighborParametersToSteadyState() throws Exception {
        final int expectedNudSolicitNum = readNudSolicitNumInSteadyStateFromResource();
        final long startTime = System.currentTimeMillis();
        // polling the "ucast_solicit" sysctl every 500ms to check if it's restored to steady
        // state, i.e. probe count of 10.
        while (getNeighborParameterUcastSolicit(mIfaceName) != expectedNudSolicitNum) {
            if (System.currentTimeMillis() - startTime >= TEST_TIMEOUT_MS) {
                final String msg = "neighbor parameter ucast_solicit isn't restored to "
                        + expectedNudSolicitNum + " within " + TEST_TIMEOUT_MS + "ms";
                fail(msg);
            }
            Thread.sleep(TEST_POLL_NEIGHBOR_PARAMETER_MS);
        }
    }

    /**
     *  A function helper to set up the steps to verify NUD (neighbor unreachable detection) probes.
     *  This function helper intends to respond to the multicast NS for the default gateway during
     *  address resolution, which makes the default gateway neighbor reachable, it ends up starting
     *  an L2 roam, which will trigger kernel to probe all neighbors later then. The specific test
     *  case may or may not respond to that probes, depending on whether it expectes an NUD failure
     *  from that probe.
     *
     *  If a specific test case expects to see an NUD failure after an L2 roam, then it should not
     *  respond to any unicast NS or multicast NS (if multicast_resolicit feature is enabled). The
     *  packet order example as below, fe80::bf8e:de37:69d7:2b29 is the IPv6 link-local address of
     *  a test tap interface.
     *
     * 7 fe80::bf8e:de37:69d7:2b29  ff02::2 ICMPv6  76  Router Solicitation from 0a:c9:06:70:77:b3
     * 9 fe80::1                    ff02::1 ICMPv6  13  Router Advertisement
     *
     * tap interface does address resolution for default gateway fe80::1 due to a DNS look-up
     * query to sent to off-link DNS server. And responding to NS with NA to make the default
     * gateway as reachable.
     * 13 2001:db8:1:0:babe:11e3:49d8:d4af  ff02::1:ff00:1     ICMPv6  92  Neighbor Solicitation for fe80::1 from 0a:c9:06:70:77:b3
     * 16 fe80::1  2001:db8:1:0:babe:11e3:49d8:d4af            ICMPv6  92  Neighbor Advertisement fe80::1 (rtr, sol) is at 00:1a:11:22:33:44
     * 17 2001:db8:1:0:1818:bf6:5e82:4378   2001:4860:4860::64 DNS     99  Standard query 0xa13e AAAA ipv4only.arpa
     *
     * Post an L2 roam, the tap interface forces kernel start another probe, it sends 5 unicast
     * NSes, the tap interface eventually gets a NUD failure due to there is no response.
     * 21 fe80::bf8e:de37:69d7:2b29   fe80::1   ICMPv6  92   Neighbor Solicitation for fe80::1 from 0a:c9:06:70:77:b3
     * 23 fe80::bf8e:de37:69d7:2b29   fe80::1   ICMPv6  92   Neighbor Solicitation for fe80::1 from 0a:c9:06:70:77:b3
     * 25 fe80::bf8e:de37:69d7:2b29   fe80::1   ICMPv6  92   Neighbor Solicitation for fe80::1 from 0a:c9:06:70:77:b3
     * 26 fe80::bf8e:de37:69d7:2b29   fe80::1   ICMPv6  92   Neighbor Solicitation for fe80::1 from 0a:c9:06:70:77:b3
     * 27 fe80::bf8e:de37:69d7:2b29   fe80::1   ICMPv6  92   Neighbor Solicitation for fe80::1 from 0a:c9:06:70:77:b3
     *
     * Extra 3 multicat NSes are sent if "mcast_resolitict" sysctl is enabled.
     * 28 fe80::bf8e:de37:69d7:2b29   ff02::1:ff00:1   ICMPv6   92    Neighbor Solicitation for fe80::1 from 0a:c9:06:70:77:b3
     * 29 fe80::bf8e:de37:69d7:2b29   ff02::1:ff00:1   ICMPv6   92    Neighbor Solicitation for fe80::1 from 0a:c9:06:70:77:b3
     * 31 fe80::bf8e:de37:69d7:2b29   ff02::1:ff00:1   ICMPv6   92    Neighbor Solicitation for fe80::1 from 0a:c9:06:70:77:b3
     */
    private void prepareIpReachabilityMonitorTest(boolean isMulticastResolicitEnabled)
            throws Exception {
        mNetworkAgentThread =
                new HandlerThread(IpClientIntegrationTestCommon.class.getSimpleName());
        mNetworkAgentThread.start();

        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();
        setFeatureEnabled(NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION,
                isMulticastResolicitEnabled);
        startIpClientProvisioning(config);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true);

        final ByteBuffer ra = buildRaPacket(TEST_IPV6_PREFIX, IPV6_OFF_LINK_DNS_SERVER,
                3600 /* validLifetime */, 1800 /* preferredLifetime */, 3600 /* dnsLifetime */,
                false /* shouldIncludeSlla */);
        // RA doesn't include SLLA option, thus device has no IPv6 link-local address and mac
        // address mapping for default gateway. sending a UDP packet to off-line DNS server will
        // trigger the address resolution for default gateway, simulate that default gateway is
        // reachable by responding to those NSes.
        final LinkProperties lp = doIpv6OnlyProvisioning(null /* inOrder */, ra);
        runAsShell(MANAGE_TEST_NETWORKS, () -> createTestNetworkAgentAndRegister(lp));
        sendPacketToPeer(ipv6Addr(IPV6_OFF_LINK_DNS_SERVER));
        expectAndRespondToMulticastNeighborSolicitation(ROUTER_LINK_LOCAL);
        assertNeverNotifyNeighborLost();

        // There is a race between the IpReachabilityMonitor handling of reachable neighbors and the
        // following test code that forces the start of L2 roaming. Once the neighbor is confirmed
        // to be still reachable, IpReachabilityMonitor restores the NUD parameters such as probe
        // count and probe interval to the steady state (probe count of 10 and interval of 750 ms).
        // Starting L2 roaming sets the NUD parameters to the post-roaming mode (probe count of 5
        // and interval of 750 ms). If the following test code occurs early, IpReachabilityMonitor
        // will first set the NUD parameters to the post-roaming state and then restore to the
        // stable state, which means that the device will send more probes, which may cause flaky
        // test because we expect to see a NUD failure event after receiving all expected probes,
        // but sometimes this does not happen because the device is still retransmitting more.
        // Polling the "unicast_solicit" sysctl to check if the IpReachabilityMonitor has already
        // restore the neighbor parameter to steady state.
        verifyRestoringNeighborParametersToSteadyState();

        // Simulate the L2 roaming, this will trigger kernel to probe all neighbors again.
        forceLayer2Roaming();
    }

    private void runIpReachabilityMonitorProbeFailedTest() throws Exception {
        prepareIpReachabilityMonitorTest();

        final int expectedNudSolicitNum = readNudSolicitNumPostRoamingFromResource();
        final List<NeighborSolicitation> nsList =
                waitForMultipleNeighborSolicitations(expectedNudSolicitNum);
        for (NeighborSolicitation ns : nsList) {
            assertUnicastNeighborSolicitation(ns, ROUTER_MAC /* dstMac */,
                    ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */);
        }
    }

    @Test
    public void testIpReachabilityMonitor_probeFailed() throws Exception {
        runIpReachabilityMonitorProbeFailedTest();
        assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */,
                NudEventType.NUD_POST_ROAMING_FAILED_CRITICAL);
    }

    @Test @SignatureRequiredTest(reason = "requires mock callback object")
    public void testIpReachabilityMonitor_probeFailed_legacyCallback() throws Exception {
        when(mCb.getInterfaceVersion()).thenReturn(12 /* assign an older interface aidl version */);

        runIpReachabilityMonitorProbeFailedTest();
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onReachabilityLost(any());
        verify(mCb, never()).onReachabilityFailure(any());
    }

    @Test
    public void testIpReachabilityMonitor_probeReachable() throws Exception {
        prepareIpReachabilityMonitorTest();

        final NeighborSolicitation ns = waitForUnicastNeighborSolicitation(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();
    }

    private void runIpReachabilityMonitorMcastResolicitProbeFailedTest() throws Exception {
        prepareIpReachabilityMonitorTest(true /* isMulticastResolicitEnabled */);

        final int expectedNudSolicitNum = readNudSolicitNumPostRoamingFromResource();
        int expectedSize = expectedNudSolicitNum + NUD_MCAST_RESOLICIT_NUM;
        final List<NeighborSolicitation> nsList =
                waitForMultipleNeighborSolicitations(expectedSize);
        for (NeighborSolicitation ns : nsList.subList(0, expectedNudSolicitNum)) {
            assertUnicastNeighborSolicitation(ns, ROUTER_MAC /* dstMac */,
                    ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */);
        }
        for (NeighborSolicitation ns : nsList.subList(expectedNudSolicitNum, nsList.size())) {
            assertMulticastNeighborSolicitation(ns, ROUTER_LINK_LOCAL /* targetIp */);
        }
    }

    @Test
    public void testIpReachabilityMonitor_mcastResolicitProbeFailed() throws Exception {
        runIpReachabilityMonitorMcastResolicitProbeFailedTest();
        assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */,
                NudEventType.NUD_POST_ROAMING_FAILED_CRITICAL);
    }

    @Test @SignatureRequiredTest(reason = "requires mock callback object")
    public void testIpReachabilityMonitor_mcastResolicitProbeFailed_legacyCallback()
            throws Exception {
        when(mCb.getInterfaceVersion()).thenReturn(12 /* assign an older interface aidl version */);

        runIpReachabilityMonitorMcastResolicitProbeFailedTest();
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onReachabilityLost(any());
        verify(mCb, never()).onReachabilityFailure(any());
    }

    @Test
    public void testIpReachabilityMonitor_mcastResolicitProbeReachableWithSameLinkLayerAddress()
            throws Exception {
        prepareIpReachabilityMonitorTest(true /* isMulticastResolicitEnabled */);

        final NeighborSolicitation ns = waitForUnicastNeighborSolicitation(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();
    }

    @Test
    public void testIpReachabilityMonitor_mcastResolicitProbeReachableWithDiffLinkLayerAddress()
            throws Exception {
        prepareIpReachabilityMonitorTest(true /* isMulticastResolicitEnabled */);

        final NeighborSolicitation ns = waitForUnicastNeighborSolicitation(ROUTER_MAC /* dstMac */,
                ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */);

        // Reply Neighbor Advertisement with a different link-layer address and check notifyLost
        // callback will be triggered. Override flag must be set, which indicates that the
        // advertisement should override an existing cache entry and update the cached link-layer
        // address, otherwise, kernel won't transit to REACHABLE state with a different link-layer
        // address.
        int flag = NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER | NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED
                | NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
        final MacAddress newMac = MacAddress.fromString("00:1a:11:22:33:55");
        final ByteBuffer na = NeighborAdvertisement.build(newMac /* srcMac */,
                ns.ethHdr.srcMac /* dstMac */, ROUTER_LINK_LOCAL /* srcIp */,
                ns.ipv6Hdr.srcIp /* dstIp */, flag, ROUTER_LINK_LOCAL /* target */);
        mPacketReader.sendResponse(na);
        assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */,
                NudEventType.NUD_POST_ROAMING_MAC_ADDRESS_CHANGED);
    }

    private ArpPacket expectAndRespondToArpRequest(MacAddress srcMac,
            Inet4Address targetIp) throws Exception {
        final ArpPacket request = getNextArpPacket();
        assertArpRequest(request, targetIp);
        sendArpReply(request.senderHwAddress.toByteArray() /* dst */, srcMac.toByteArray(),
                request.senderIp /* target IP */, targetIp /* sender IP */);
        return request;
    }

    /**
     * A function helper to set up the steps to verify NUD (neighbor unreachable detection) probes
     * for IPv4 hosts. This function helper intends to respond to the ARP probes for the default
     * gateway during address resolution if the param shouldMakeNeighborReachableFirst is true,
     * which makes the default gateway reachable first, and then sending a gratuitous ARP with a
     * different mac address, it will override the ARP entry on the tap interface, as a result, it
     * should do the address resolution again. The param shouldMakeNeighborReachableFirst depends on
     * the specific test case.
     *
     * If a specific test case expectes to see an NUD failure after that, then it should not respond
     * to any upcoming ARP probes. The packet order example as below,
     *
     * 37  0.0.0.0        255.255.255.255  DHCP  338   DHCP Discover - Transaction ID 0xf367c95c
     * 38  192.168.1.100  192.168.1.2      DHCP  360   DHCP ACK      - Transaction ID 0xf367c95c
     *
     * 45  66:64:50:87:c3:22  ARP  48  Who has 192.168.1.100? Tell 192.168.1.2
     * 46  Google_22:33:44    ARP  48  192.168.1.100 is at 00:1a:11:22:33:44
     * 47  192.168.1.2  192.168.1.100  UDP  148  47467 1234 Len=100
     *
     * 76  Google_22:33:55  ARP  48   Gratuitous ARP for 192.168.1.100 (Reply) (duplicate use of 192.168.1.100 detected!)
     * 77  192.168.1.2  192.168.1.100  UDP  148  38374  1234 Len=100
     *
     * 92  66:64:50:87:c3:22  ARP  48  Who has 192.168.1.100? Tell 192.168.1.2
     * 93  66:64:50:87:c3:22  ARP  48  Who has 192.168.1.100? Tell 192.168.1.2
     * ...
     * 116 66:64:50:87:c3:22  ARP  48  Who has 192.168.1.100? Tell 192.168.1.2
     */
    private void prepareIpReachabilityMonitorIpv4AddressResolutionTest(
            boolean shouldMakeNeighborReachableFirst) throws Exception {
        // Reduce the delay first probe time from 5s to 2s, speed up the test duration and we can
        // still use PACKET_TIMEOUT_MS to wait for the next upcoming ARP packet.
        setNudDelayFirstProbeTime(TEST_DELAY_FIRST_PROBE_TIME_S, "ipv4");
        mNetworkAgentThread =
                new HandlerThread(IpClientIntegrationTestCommon.class.getSimpleName());
        mNetworkAgentThread.start();

        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIPv6()
                .build();
        setDhcpFeatures(true /* isRapidCommitEnabled */, false /* isDhcpIpConflictDetectEnabled */);
        startIpClientProvisioning(config);

        // Start IPv4 provisioning and wait until entire provisioning completes.
        handleDhcpPackets(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
                true /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU, null /* serverSentUrl */);
        final LinkProperties lp =
                verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));

        runAsShell(MANAGE_TEST_NETWORKS, () -> createTestNetworkAgentAndRegister(lp));

        // Send a UDP packet to IPv4 on-link DNS server to trigger address resolution process for
        // the default gateway, respond to the broadcast ARP probe and make the default gateway as
        // reachable.
        sendPacketToPeer(SERVER_ADDR);
        if (shouldMakeNeighborReachableFirst) {
            final ArpPacket request = expectAndRespondToArpRequest(ROUTER_MAC, SERVER_ADDR);
            assertNotNull(request);
            verifyRestoringNeighborParametersToSteadyState();

            // Wait the locktime expires then we are able to override ARP entry by sending a
            // gratuitous ARP with a different MAC address, see locktime sysctl on
            // https://man7.org/linux/man-pages/man7/arp.7.html.
            Thread.sleep(TEST_ARP_LOCKTIME_MS);

            // Send a gratuitous ARP to override the default gateway MAC address, this makes the
            // default gateway become stale in the ARP entry, sending another UDP packet to the
            // default gateway make it transit to DELAY state. If no reachability confirmation is
            // received within DELAY_FIRST_PROBE_TIME seconds of entering the DELAY state, kernel
            // goes to PROBE state and start probing, see RFC 4861 section 7.3.2.
            final MacAddress newMac = MacAddress.fromString("00:1A:11:22:33:55");
            sendGratuitousArp(newMac, SERVER_ADDR);
            sendPacketToPeer(SERVER_ADDR);
        }
    }

    private void doTestIpReachabilityMonitor_replyBroadcastArpRequestWithDiffMacAddresses(
            boolean disconnect) throws Exception {
        prepareIpReachabilityMonitorIpv4AddressResolutionTest(
                false /* shouldMakeNeighborReachableFirst */);

        // Respond to the broadcast ARP request.
        final ArpPacket request = getNextArpPacket();
        assertArpRequest(request, SERVER_ADDR);
        sendArpReply(request.senderHwAddress.toByteArray() /* dst */, ROUTER_MAC_BYTES /* srcMac */,
                request.senderIp /* target IP */, SERVER_ADDR /* sender IP */);

        Thread.sleep(TEST_ARP_LOCKTIME_MS);

        // Reply with a different MAC address but the same server IP.
        final MacAddress gateway = MacAddress.fromString("00:11:22:33:44:55");
        sendArpReply(request.senderHwAddress.toByteArray() /* dst */,
                gateway.toByteArray() /* srcMac */,
                request.senderIp /* target IP */, SERVER_ADDR /* sender IP */);

        if (disconnect) {
            final ArgumentCaptor<ReachabilityLossInfoParcelable> lossInfoCaptor =
                    ArgumentCaptor.forClass(ReachabilityLossInfoParcelable.class);
            verify(mCb, timeout(TEST_TIMEOUT_MS)).onReachabilityFailure(lossInfoCaptor.capture());
            assertEquals(ReachabilityLossReason.ORGANIC, lossInfoCaptor.getValue().reason);
        } else {
            verify(mCb, after(100).never()).onReachabilityFailure(any());
        }
    }

    @Test
    public void testIpReachabilityMonitor_macAddressChangedWithoutRoam_ok()
            throws Exception {
        setFeatureChickenedOut(IP_REACHABILITY_ROUTER_MAC_CHANGE_FAILURE_ONLY_AFTER_ROAM_VERSION,
                false);
        doTestIpReachabilityMonitor_replyBroadcastArpRequestWithDiffMacAddresses(false);
    }

    @Test
    public void testIpReachabilityMonitor_macAddressChangedWithoutRoam_disconnect()
            throws Exception {
        setFeatureChickenedOut(IP_REACHABILITY_ROUTER_MAC_CHANGE_FAILURE_ONLY_AFTER_ROAM_VERSION,
                true);
        doTestIpReachabilityMonitor_replyBroadcastArpRequestWithDiffMacAddresses(true);
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = true)
    public void testIpReachabilityMonitor_ignoreIpv4DefaultRouterOrganicNudFailure()
            throws Exception {
        prepareIpReachabilityMonitorIpv4AddressResolutionTest(
                true /* shouldMakeNeighborReachableFirst */
        );

        ArpPacket packet;
        while ((packet = getNextArpPacket(TEST_TIMEOUT_MS)) != null) {
            // wait address resolution to complete.
        }
        verify(mCb, never()).onReachabilityFailure(any());
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = false)
    public void testIpReachabilityMonitor_ignoreIpv4DefaultRouterOrganicNudFailure_flagoff()
            throws Exception {
        prepareIpReachabilityMonitorIpv4AddressResolutionTest(
                true /* shouldMakeNeighborReachableFirst */);

        ArpPacket packet;
        while ((packet = getNextArpPacket(PACKET_TIMEOUT_MS)) != null) {
            // wait address resolution to complete.
        }
        final ArgumentCaptor<ReachabilityLossInfoParcelable> lossInfoCaptor =
                ArgumentCaptor.forClass(ReachabilityLossInfoParcelable.class);
        verify(mCb).onReachabilityFailure(lossInfoCaptor.capture());
        assertEquals(ReachabilityLossReason.ORGANIC, lossInfoCaptor.getValue().reason);
    }

    private void sendUdpPacketToNetwork(final Network network, final InetAddress remoteIp,
            int port, final byte[] data) throws Exception {
        final InetAddress laddr =
                (remoteIp instanceof Inet6Address) ? Inet6Address.ANY : Inet4Address.ANY;
        final DatagramSocket socket = new DatagramSocket(0, laddr);
        final DatagramPacket pkt = new DatagramPacket(data, data.length, remoteIp, port);
        network.bindSocket(socket);
        socket.send(pkt);
    }

    /**
     * A function helper to set up the steps to verify NUD (neighbor unreachable detection) probes
     * for IPv6 hosts. This function helper intends to respond to the NS probes for the default
     * gateway during address resolution if the param shouldMakeNeighborReachableFirst is true,
     * which makes the default gateway reachable first, and then sending a gratuitous NA with a
     * different mac address, it will override the neighbor entry on the tap interface, as a result,
     * it should do the address resolution eventually. The param shouldMakeNeighborReachableFirst
     * depends on the specific test case which may or may not expect a NUD failure event.
     *
     * 36  2025-02-12 13:19:11.316069  fe80::1e3a:4b22:df15:896c  ff02::2  ICMPv6  76   Router Solicitation from 46:0c:b1:ab:47:f7
     * 46  2025-02-12 13:19:11.409184  fe80::1  ff02::1                    ICMPv6  132  Router Advertisement
     *
     * // Respond to the NS for the default gateway and make the default gateay as reachable.
     * 50  2025-02-12 13:19:11.443185  2001:db8:1:0:d8d6:93c1:9620:2a37  ff02::1:ff00:1      ICMPv6  92  Neighbor Solicitation for fe80::1 from 46:0c:b1:ab:47:f7
     * 51  2025-02-12 13:19:11.448864  2001:db8:1:0:48c7:9fe:4ee:47ad    ff02::1:ff00:1      ICMPv6  92  Neighbor Solicitation for fe80::1 from 46:0c:b1:ab:47:f7
     * 52  2025-02-12 13:19:11.522637  2001:db8:1:0:48c7:9fe:4ee:47ad    ff02::1:ff00:1      ICMPv6  92  Neighbor Solicitation for fe80::1 from 46:0c:b1:ab:47:f7
     * 53  2025-02-12 13:19:11.523851  fe80::1  2001:db8:1:0:d8d6:93c1:9620:2a37             ICMPv6  92  Neighbor Advertisement fe80::1 (rtr, sol) is at 00:1a:11:22:33:44
     * 54  2025-02-12 13:19:11.523881  2001:db8:1:0:48c7:9fe:4ee:47ad    2001:4860:4860::64  UDP  168  49350 → 1234 Len=100
     *
     * 55  2025-02-12 13:19:11.535166  fe80::1  ff02::1  ICMPv6  92  Neighbor Advertisement fe80::1 (rtr, ovr) is at 00:11:22:33:44:55
     * 56  2025-02-12 13:19:11.535853  2001:db8:1:0:48c7:9fe:4ee:47ad  2001:4860:4860::64  UDP  168  36306 → 1234 Len=100
     *
     * // After test_delay_first_probe_time(2s) the tap interface starts probing again.
     * 99   2025-02-12 13:19:13.544188  fe80::1e3a:4b22:df15:896c  fe80::1         ICMPv6  92  Neighbor Solicitation for fe80::1 from 46:0c:b1:ab:47:f7
     * 107  2025-02-12 13:19:14.312212  fe80::1e3a:4b22:df15:896c  fe80::1         ICMPv6  92  Neighbor Solicitation for fe80::1 from 46:0c:b1:ab:47:f7
     * ...
     * 133  2025-02-12 13:19:20.456209  fe80::1e3a:4b22:df15:896c  fe80::1         ICMPv6  92  Neighbor Solicitation for fe80::1 from 46:0c:b1:ab:47:f7
     * 134  2025-02-12 13:19:21.224214  fe80::1e3a:4b22:df15:896c  ff02::1:ff00:1  ICMPv6  92  Neighbor Solicitation for fe80::1 from 46:0c:b1:ab:47:f7
     * 135  2025-02-12 13:19:21.992192  fe80::1e3a:4b22:df15:896c  ff02::1:ff00:1  ICMPv6  92  Neighbor Solicitation for fe80::1 from 46:0c:b1:ab:47:f7
     * 136  2025-02-12 13:19:22.760202  fe80::1e3a:4b22:df15:896c  ff02::1:ff00:1  ICMPv6  92  Neighbor Solicitation for fe80::1 from 46:0c:b1:ab:47:f7
     */
    private void prepareIpReachabilityMonitorAddressResolutionTest(final String dnsServer,
            final Inet6Address targetIp,
            boolean shouldMakeNeighborReachableFirst) throws Exception {
        // Reduce the delay first probe time from 5s to 2s, speed up the test duration and we can
        // still use PACKET_TIMEOUT_MS to wait for the next upcoming ARP packet.
        setNudDelayFirstProbeTime(TEST_DELAY_FIRST_PROBE_TIME_S, "ipv6");

        mNetworkAgentThread =
                new HandlerThread(IpClientIntegrationTestCommon.class.getSimpleName());
        mNetworkAgentThread.start();

        setDhcpFeatures(true /* isRapidCommitEnabled */,
                false /* isDhcpIpConflictDetectEnabled */);
        final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                // We've found that mCm.shouldAvoidBadWifi() has a flaky behavior in the root test,
                // probably due to the sim card in the DUT. it doesn't occur in the siganture test
                // since we mock the return value directly. As a result, sometimes
                // IpReachabilityMonitor#avoidingBadLinks() returns false, it caused the expected
                // onReachabilityFailure callback wasn't triggered on the test. In order to make
                // the root test more stable, do not use MultinetworkPolicyTracker only for IPv6
                // neighbor reachability checking relevant test cases, that guarantees
                // avoidingBadLinks() always returns true which is expected.
                .withoutMultinetworkPolicyTracker()
                // Make cluster as non-null to test the NUD failure event count query logic.
                .withLayer2Information(new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
                       MacAddress.fromString(TEST_DEFAULT_BSSID)))
                .build();
        startIpClientProvisioning(config);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true);

        final ByteBuffer ra = buildRaPacket(TEST_IPV6_PREFIX, dnsServer,
                3600 /* validLifetime */, 1800 /* preferredLifetime */, 3600 /* dnsLifetime */,
                !targetIp.equals(ROUTER_LINK_LOCAL) /* shouldIncludeSlla */);
        final Inet6Address dnsServerIp = ipv6Addr(dnsServer);
        final LinkProperties lp = performDualStackProvisioning(ra, dnsServerIp);
        runAsShell(MANAGE_TEST_NETWORKS, () -> createTestNetworkAgentAndRegister(lp));

        if (shouldMakeNeighborReachableFirst) {
            sendPacketToPeer(dnsServerIp);
            expectAndRespondToMulticastNeighborSolicitation(targetIp);
            assertNeverNotifyNeighborLost();
            verifyRestoringNeighborParametersToSteadyState();

            // Send a gratuitous NA for the target neighbor with a different MAC address, this
            // should make the neighbor state transit from REACHABLE to STALE, sending another UDP
            // packet to neighbor will force the neighbor state transit from STALE to DELAY, and
            // after DELAY_FIRST_PROBE_TIME the tap interface starts probing for the target neighbor
            // again.
            final MacAddress srcMac = MacAddress.fromString("00:11:22:33:44:55");
            final Inet6Address srcIp = targetIp.equals(ROUTER_LINK_LOCAL)
                    ? ROUTER_LINK_LOCAL
                    : ipv6Addr(IPV6_ON_LINK_DNS_SERVER);
            sendGratuitousNeighborAdvertisement(srcMac, srcIp, targetIp);
        }
    }

    /**
     * Send a UDP packet to dstIp to trigger address resolution for targetIp, and possibly expect a
     * neighbor lost callback.
     * If dstIp is on-link, then dstIp and targetIp should be the same.
     * If dstIp is off-link, then targetIp should be the IPv6 default router.
     * The ND cache should not have an entry for targetIp.
     */
    private void sendPacketToPeer(final InetAddress dstIp) throws Exception {
        final Random random = new Random();
        final byte[] data = new byte[100];
        random.nextBytes(data);
        sendUdpPacketToNetwork(mNetworkAgent.getNetwork(), dstIp, 1234 /* port */, data);
    }

    private void expectAndDropMultipleNses(Inet6Address targetIp, boolean expectNeighborLost)
            throws Exception {
        // Wait for the multiple NSes but never respond to them, that results in the on-link
        // DNS or default gateway gets lost and onReachabilityLost callback will be invoked.
        final List<NeighborSolicitation> nsList = new ArrayList<NeighborSolicitation>();
        NeighborSolicitation ns;
        while ((ns = getNextNeighborSolicitation()) != null) {
            // multiple NSes for address resolution, a few unicast NSes will be sent first and
            // fall back to multicast if mcast_resolicit is enabled. IPv6 dst address in multicast
            // NS is solicited-node multicast address based on the target IP, the target IP is
            // either on-link IPv6 DNS server address or IPv6 link-local address of default gateway.
            final LinkAddress actual = new LinkAddress(ns.nsHdr.target, 64);
            final LinkAddress target = new LinkAddress(targetIp, 64);
            if (actual.equals(target)) {
                nsList.add(ns);
            }
        }
        assertFalse(nsList.isEmpty());

        if (expectNeighborLost) {
            assertNotifyNeighborLost(targetIp, NudEventType.NUD_ORGANIC_FAILED_CRITICAL);
        } else {
            assertNeverNotifyNeighborLost();
        }
    }

    private void runIpReachabilityMonitorAddressResolutionTest(final String dnsServer,
            final Inet6Address targetIp,
            final boolean shouldMakeNeighborReachableFirst,
            final boolean expectNeighborLost) throws Exception {
        prepareIpReachabilityMonitorAddressResolutionTest(dnsServer, targetIp,
                shouldMakeNeighborReachableFirst);
        sendPacketToPeer(ipv6Addr(dnsServer));
        expectAndDropMultipleNses(targetIp, expectNeighborLost);
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = false)
    public void testIpReachabilityMonitor_incompleteIpv6DnsServerInDualStack() throws Exception {
        final Inet6Address targetIp = ipv6Addr(IPV6_ON_LINK_DNS_SERVER);
        runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER,
                targetIp,
                false /* shouldMakeNeighborReachableFirst */,
                false /* expectNeighborLost */);
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = false)
    public void testIpReachabilityMonitor_incompleteIpv6DnsServerInDualStack_flagoff()
            throws Exception {
        final Inet6Address targetIp = ipv6Addr(IPV6_ON_LINK_DNS_SERVER);
        runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER,
                targetIp,
                false /* shouldMakeNeighborReachableFirst */,
                false /* expectNeighborLost */);
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = false)
    public void testIpReachabilityMonitor_incompleteIpv6DefaultRouterInDualStack()
            throws Exception {
        runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER,
                ROUTER_LINK_LOCAL /* targetIp */,
                false /* shouldMakeNeighborReachableFirst */,
                false /* expectNeighborLost */);
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = false)
    public void testIpReachabilityMonitor_incompleteIpv6DefaultRouterInDualStack_flagoff()
            throws Exception {
        runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER,
                ROUTER_LINK_LOCAL /* targetIp */,
                false /* shouldMakeNeighborReachableFirst */,
                false /* expectNeighborLost */);
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = true)
    public void testIpReachabilityMonitor_ignoreOnLinkIpv6DnsOrganicNudFailure()
            throws Exception {
        final Inet6Address targetIp = ipv6Addr(IPV6_ON_LINK_DNS_SERVER);
        runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER,
                targetIp,
                true /* shouldMakeNeighborReachableFirst */,
                false /* expectNeighborLost */);
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = false)
    public void testIpReachabilityMonitor_ignoreOnLinkIpv6DnsOrganicNudFailure_flagoff()
            throws Exception {
        final Inet6Address targetIp = ipv6Addr(IPV6_ON_LINK_DNS_SERVER);
        runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER,
                targetIp,
                false /* shouldMakeNeighborReachableFirst */,
                false /* expectNeighborLost */);
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = true)
    public void testIpReachabilityMonitor_ignoreIpv6DefaultRouterOrganicNudFailure()
            throws Exception {
        runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER,
                ROUTER_LINK_LOCAL /* targetIp */,
                true /* shouldMakeNeighborReachableFirst */,
                false /* expectNeighborLost */);
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = false)
    public void testIpReachabilityMonitor_ignoreIpv6DefaultRouterOrganicNudFailure_flagoff()
            throws Exception {
        runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER,
                ROUTER_LINK_LOCAL /* targetIp */,
                true /* shouldMakeNeighborReachableFirst */,
                true /* expectNeighborLost */);
    }

    private void runIpReachabilityMonitorEverReachableIpv6NeighborTest(final String dnsServer,
            final Inet6Address targetIp) throws Exception {
        prepareIpReachabilityMonitorAddressResolutionTest(dnsServer, targetIp,
                false /*shouldMakeNeighborReachableFirst */);
        sendPacketToPeer(ipv6Addr(dnsServer));

        // Simulate the default router/DNS was reachable by responding to multicast NS(not for DAD).
        NeighborSolicitation ns;
        while ((ns = getNextNeighborSolicitation()) != null) {
            if (ns.ipv6Hdr.dstIp.isMulticastAddress() // Solicited-node multicast address
                    && ns.nsHdr.target.equals(targetIp)) {
                final ByteBuffer na = NeighborAdvertisement.build(ROUTER_MAC /* srcMac */,
                        ns.ethHdr.srcMac /* dstMac */, ROUTER_LINK_LOCAL /* srcIp */,
                        ns.ipv6Hdr.srcIp /* dstIp */,
                        NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER | NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED,
                        targetIp);
                mPacketReader.sendResponse(na);
                break;
            }
        }

        // Trigger the NUD probe manually by sending CMD_CONFIRM command, this will force to start
        // probing for all neighbors in the watchlist including default router and on-link DNS.
        mIIpClient.confirmConfiguration();

        // Wait for the next unicast NS probes, but don't respond to them, which should trigger
        // reachability failure callback because the probe status is from probed to failed, rather
        // than incomplete to failed.
        while ((ns = getNextNeighborSolicitation()) != null) {
            // Respond to NS for default router, it's used to avoid triggering multiple
            // onReachabilityFailure callbacks.
            if (!targetIp.equals(ROUTER_LINK_LOCAL)) {
                final ByteBuffer na = NeighborAdvertisement.build(ROUTER_MAC /* srcMac */,
                        ns.ethHdr.srcMac /* dstMac */, ROUTER_LINK_LOCAL /* srcIp */,
                        ns.ipv6Hdr.srcIp /* dstIp */,
                        NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER | NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED,
                        ROUTER_LINK_LOCAL);
                mPacketReader.sendResponse(na);
            }
        }
        assertNotifyNeighborLost(targetIp, NudEventType.NUD_CONFIRM_FAILED_CRITICAL);
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = false)
    public void testIpReachabilityMonitor_ignoreIpv6DefaultRouter_everReachable() throws Exception {
        runIpReachabilityMonitorEverReachableIpv6NeighborTest(IPV6_OFF_LINK_DNS_SERVER,
                ROUTER_LINK_LOCAL /* targetIp */);
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = false)
    public void testIpReachabilityMonitor_ignoreIpv6Dns_everReachable() throws Exception {
        runIpReachabilityMonitorEverReachableIpv6NeighborTest(IPV6_ON_LINK_DNS_SERVER,
                ipv6Addr(IPV6_ON_LINK_DNS_SERVER) /* targetIp */);
    }

    @Test
    public void testIpReachabilityMonitor_ignoreNeverReachableIpv6Dns() throws Exception {
        runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER,
                ipv6Addr(IPV6_ON_LINK_DNS_SERVER),
                false /* shouldMakeNeighborReachableFirst */,
                false /* expectNeighborLost */);
    }

    @Test
    public void testIpReachabilityMonitor_ignoreNeverReachableIpv6Dns_butEverReachable()
            throws Exception {
        runIpReachabilityMonitorEverReachableIpv6NeighborTest(IPV6_ON_LINK_DNS_SERVER,
                ipv6Addr(IPV6_ON_LINK_DNS_SERVER) /* targetIp */);
    }

    @Test
    public void testIpReachabilityMonitor_ignoreNeverReachableIpv6DefaultRouter() throws Exception {
        runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER,
                ROUTER_LINK_LOCAL,
                false /* shouldMakeNeighborReachableFirst */,
                false /* expectNeighborLost */);
    }

    @Test
    public void testIpReachabilityMonitor_ignoreNeverReachableIpv6DefaultRouter_butEverReachable()
            throws Exception {
        runIpReachabilityMonitorEverReachableIpv6NeighborTest(IPV6_ON_LINK_DNS_SERVER,
                ROUTER_LINK_LOCAL /* targetIp */);
    }

    @Test
    public void testIPv6LinkLocalOnly() throws Exception {
        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIPv4()
                .withIpv6LinkLocalOnly()
                .withRandomMacAddress()
                .build();
        startIpClientProvisioning(config);

        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
        final LinkProperties lp = captor.getValue();
        assertNotNull(lp);
        assertEquals(0, lp.getDnsServers().size());
        final List<LinkAddress> addresses = lp.getLinkAddresses();
        assertEquals(1, addresses.size());
        assertTrue(addresses.get(0).getAddress().isLinkLocalAddress()); // only IPv6 link-local
        assertTrue(hasRouteTo(lp, IPV6_LINK_LOCAL_PREFIX)); // fe80::/64 -> :: iface mtu 0

        // Check that if an RA is received, no IP addresses, routes, or DNS servers are configured.
        // Instead of waiting some period of time for the RA to be received and checking the
        // LinkProperties after that, tear down the interface and wait for it to go down. Then check
        // that no LinkProperties updates ever contained non-link-local information.
        sendBasicRouterAdvertisement(false /* waitForRs */);
        teardownTapInterface();
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningFailure(any());
        verify(mCb, never()).onLinkPropertiesChange(argThat(newLp ->
                // Ideally there should be only one route(fe80::/64 -> :: iface mtu 0) in the
                // LinkProperties, however, the multicast route(ff00::/8 -> :: iface mtu 0) may
                // appear on some old platforms where the kernel is still notifying the userspace
                // the multicast route. Therefore, we cannot assert that size of routes in the
                // LinkProperties is more than one, but other properties such as DNS or IPv6
                // default route or global IPv6 address should never appear in the IPv6 link-local
                // only mode.
                newLp.getDnsServers().size() != 0
                        || newLp.hasIpv6DefaultRoute()
                        || newLp.hasGlobalIpv6Address()
        ));
    }

    @Test
    public void testIPv6LinkLocalOnly_verifyAcceptRaDefrtr() throws Exception {
        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIPv4()
                .withIpv6LinkLocalOnly()
                .withRandomMacAddress()
                .build();
        startIpClientProvisioning(config);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(any());

        clearInvocations(mCb);

        // accept_ra is set to 0 and accept_ra_defrtr is set to 1 in IPv6 link-local only mode,
        // send another RA to tap interface, to verify that we should not see any IPv6 provisioning
        // although accept_ra_defrtr is set to 1.
        sendBasicRouterAdvertisement(false /* waitForRs */);
        verify(mCb, never()).onLinkPropertiesChange(argThat(x -> x.isIpv6Provisioned()));
    }

    @Test
    public void testIPv6LinkLocalOnlyAndThenGlobal() throws Exception {
        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIPv4()
                .withIpv6LinkLocalOnly()
                .withRandomMacAddress()
                .build();
        startIpClientProvisioning(config);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(any());
        mIIpClient.stop();
        verifyAfterIpClientShutdown();
        reset(mCb);

        // Speed up provisioning by enabling rapid commit. TODO: why is this necessary?
        setDhcpFeatures(true /* isRapidCommitEnabled */,
                false /* isDhcpIpConflictDetectEnabled */);
        config = new ProvisioningConfiguration.Builder()
                .build();
        startIpClientProvisioning(config);
        performDualStackProvisioning();
        // No exceptions? Dual-stack provisioning worked.
    }

    @Test
    public void testIPv6LinkLocalOnly_enableBothIPv4andIPv6LinkLocalOnly() throws Exception {
        assertThrows(IllegalArgumentException.class,
                () -> new ProvisioningConfiguration.Builder()
                        .withoutIpReachabilityMonitor()
                        .withIpv6LinkLocalOnly()
                        .withRandomMacAddress()
                        .build()
        );
    }

    private void runIpv6LinkLocalOnlyDadTransmitsCheckTest(boolean shouldDisableDad)
            throws Exception {
        ProvisioningConfiguration.Builder config = new ProvisioningConfiguration.Builder()
                .withoutIPv4()
                .withIpv6LinkLocalOnly()
                .withRandomMacAddress();
        if (shouldDisableDad) config.withUniqueEui64AddressesOnly();

        // dad_transmits has been set to 0 in disableIpv6ProvisioningDelays, re-enable dad_transmits
        // for testing, but production code could disable dad again later, we should never see any
        // multicast NS for duplicate address detection then.
        mNetd.setProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "dad_transmits", "1");
        startIpClientProvisioning(config.build());
        verify(mNetd, timeout(TEST_TIMEOUT_MS)).interfaceSetEnableIPv6(mIfaceName, true);
        // Check dad_transmits should be set to 0 if UniqueEui64AddressesOnly mode is enabled.
        int dadTransmits = Integer.parseUnsignedInt(
                mNetd.getProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "dad_transmits"));
        if (shouldDisableDad) {
            assertEquals(0, dadTransmits);
        } else {
            assertEquals(1, dadTransmits);
        }

        final NeighborSolicitation ns =
                expectDadNeighborSolicitationForLinkLocal(shouldDisableDad);
        if (shouldDisableDad) {
            assertNull(ns);
        } else {
            assertNotNull(ns);
        }

        // Shutdown IpClient and check if the dad_transmits always equals to default value 1 (if
        // dad_transmit was set to 0 before, it should get recovered to default value 1 after
        // shutting down IpClient)
        mIpc.shutdown();
        awaitIpClientShutdown();
        dadTransmits = Integer.parseUnsignedInt(
                mNetd.getProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "dad_transmits"));
        assertEquals(1, dadTransmits);
    }

    @Test
    @SignatureRequiredTest(reason = "requires mocked netd")
    public void testIPv6LinkLocalOnly_enableDad() throws Exception {
        runIpv6LinkLocalOnlyDadTransmitsCheckTest(false /* shouldDisableDad */);
    }

    @Test
    @SignatureRequiredTest(reason = "requires mocked netd")
    public void testIPv6LinkLocalOnly_disableDad() throws Exception {
        runIpv6LinkLocalOnlyDadTransmitsCheckTest(true /* shouldDisableDad */);
    }

    // Since createTapInterface(boolean, String) method was introduced since T, this method
    // cannot be found on Q/R/S platform, ignore this test on T- platform.
    @Test
    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
    public void testIpClientLinkObserver_onClatInterfaceStateUpdate() throws Exception {
        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIPv4()
                .build();
        startIpClientProvisioning(config);
        doIpv6OnlyProvisioning();

        reset(mCb);

        // Add the clat interface and check the callback.
        final TestNetworkInterface clatIface = setUpClatInterface(mIfaceName);
        assertNotNull(clatIface);
        assertTrue(clatIface.getInterfaceName().equals(CLAT_PREFIX + mIfaceName));
        verify(mCb, timeout(TEST_TIMEOUT_MS)).setNeighborDiscoveryOffload(false);

        // Remove the clat interface and check the callback.
        removeTestInterface(clatIface.getFileDescriptor().getFileDescriptor());
        verify(mCb, timeout(TEST_TIMEOUT_MS)).setNeighborDiscoveryOffload(true);
    }

    @ConnectivityDiagnosticsCollector.CollectTcpdumpOnFailure
    @Test
    @SignatureRequiredTest(reason = "requires mock callback object")
    public void testNetlinkSocketReceiveENOBUFS() throws Exception {
        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIPv4()
                .build();
        startIpClientProvisioning(config);
        doIpv6OnlyProvisioning();
        HandlerUtils.waitForIdle(mIpc.getHandler(), TEST_TIMEOUT_MS);

        final Handler handler = mIpc.getHandler();
        // Block IpClient handler.
        final CountDownLatch latch = new CountDownLatch(1);
        handler.post(() -> {
            try {
                latch.await(10, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                fail("latch wait unexpectedly interrupted");
            }
        });

        // Send large amount of RAs to overflow the netlink socket receive buffer.
        for (int i = 0; i < 200; i++) {
            sendBasicRouterAdvertisement(false /* waitRs */);
        }

        // Send another RA with a different IPv6 global prefix. This PIO option should be dropped
        // due to the ENOBUFS happens, it means IpClient shouldn't see the new IPv6 global prefix.
        final String prefix = "2001:db8:dead:beef::/64";
        final ByteBuffer pio = buildPioOption(3600, 1800, prefix);
        ByteBuffer rdnss = buildRdnssOption(3600, IPV6_OFF_LINK_DNS_SERVER);
        sendRouterAdvertisement(false /* waitForRs */, (short) 1800, pio, rdnss);

        // Unblock the IpClient handler and ENOBUFS should happen then.
        latch.countDown();
        HandlerUtils.waitForIdle(handler, TEST_WAIT_ENOBUFS_TIMEOUT_MS);

        reset(mCb);

        // Send RA with 0 router lifetime to see if IpClient can see the loss of IPv6 default route.
        // Due to ignoring the ENOBUFS and wait until handler gets idle, IpClient should be still
        // able to see the RA with 0 router lifetime and the IPv6 default route will be removed.
        // LinkProperties should not include any route to the new prefix 2001:db8:dead:beef::/64.
        sendRouterAdvertisementWithZeroRouterLifetime();
        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningFailure(captor.capture());
        final LinkProperties lp = captor.getValue();
        assertNotNull(lp);
        assertFalse(hasRouteTo(lp, prefix));
        assertFalse(lp.hasIpv6DefaultRoute());
    }

    @Test
    public void testMulticastNsFromIPv6Gua() throws Exception {
        final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIpReachabilityMonitor()
                .withoutIPv4()
                .build();

        startIpClientProvisioning(config);

        doIpv6OnlyProvisioning();

        final List<NeighborSolicitation> nsList = new ArrayList<>();
        NeighborSolicitation packet;
        while ((packet = getNextNeighborSolicitation()) != null) {
            // Filter out the NSes used for duplicate address detetction, whose target address
            // is the global IPv6 address inside these NSes.
            if (packet.nsHdr.target.isLinkLocalAddress()) {
                assertMulticastNsFromIpv6Gua(packet);
                nsList.add(packet);
            }
        }
        assertEquals(2, nsList.size()); // from privacy address and stable privacy address
    }

    @Test
    public void testDeprecatedGlobalUnicastAddress() throws Exception {
        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIPv4()
                .build();
        startIpClientProvisioning(config);
        doIpv6OnlyProvisioning();

        // Send RA with PIO(0 preferred but valid lifetime) to deprecate the global IPv6 addresses.
        // Check all of global IPv6 addresses will become deprecated, but still valid.
        // NetworkStackUtils#isIPv6GUA() will return false for deprecated addresses, however, when
        // checking if the DNS is still reachable, deprecated addresses are not acceptable, that
        // results in the on-link DNS server gets lost from LinkProperties, and provisioning failure
        // happened.
        // TODO: update the logic of checking reachable on-link DNS server to accept the deprecated
        // addresses, then onProvisioningFailure callback should never happen.
        sendRouterAdvertisement(false /* waitForRs*/, (short) 1800 /* router lifetime */,
                3600 /* valid */, 0 /* preferred */);
        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningFailure(captor.capture());
        final LinkProperties lp = captor.getValue();
        assertNotNull(lp);
        assertFalse(lp.hasGlobalIpv6Address());
        assertEquals(3, lp.getLinkAddresses().size()); // IPv6 privacy, stable privacy, link-local
        for (LinkAddress la : lp.getLinkAddresses()) {
            assertFalse(NetworkStackUtils.isIPv6GUA(la));
        }
    }

    @Test
    public void testKernelDeletesIPv6AddressesOnValidLifetimeExpires() throws Exception {
        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIPv4()
                .build();
        startIpClientProvisioning(config);

        // Intend to set the same preferred and valid lifetime in RA PIO to 3s. All global IPv6
        // addresses will be deleted from the interface when the valid lifetime expires, this might
        // be caused by the loss of RA due to the DTIM config. Then a onProvisioningFailure event
        // will be triggered if that's the IPv6-only network.
        final ByteBuffer ra = buildRaPacket(TEST_IPV6_PREFIX, IPV6_ON_LINK_DNS_SERVER,
                3 /* validLifetime */, 3 /* preferredLifetime */, 600 /* dnsLifetime */,
                true /* shouldIncludeSlla */);
        doIpv6OnlyProvisioning(null /* inOrder */, ra);

        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(PACKET_TIMEOUT_MS)).onProvisioningFailure(captor.capture());
        final LinkProperties lp = captor.getValue();
        assertNotNull(lp);
        assertFalse(lp.hasGlobalIpv6Address());
        assertEquals(1, lp.getLinkAddresses().size()); // only IPv6 Link-local address
        // because the DNS server is on-link, if off-link, due to the loss of IPv6 address, off-link
        // DNS dest will be removed from LP as well.
        assertTrue(lp.hasIpv6DnsServer());
        assertTrue(lp.hasIpv6DefaultRoute());
    }

    @Test @SignatureRequiredTest(reason = "requires mNetd to delete IPv6 GUAs")
    public void testOnIpv6AddressRemoved() throws Exception {
        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIPv4()
                .build();
        startIpClientProvisioning(config);

        LinkProperties lp = doIpv6OnlyProvisioning();
        assertNotNull(lp);
        assertEquals(3, lp.getLinkAddresses().size()); // IPv6 privacy, stable privacy, link-local

        final LinkAddress privacyAddress =
                IpClient.find(lp.getLinkAddresses(), this::isPrivacyAddress);
        final LinkAddress stableAddress =
                IpClient.find(lp.getLinkAddresses(), this::isStablePrivacyAddress);
        assertNotNull(privacyAddress);
        assertNotNull(stableAddress);

        // Delete the temporary privacy address before deleting the stable privacy address.
        // Otherwise, deleting the stable privacy address will also delete the associated
        // temporary privacy address. If this happens first, then deleting the non-existent
        // temporary privacy address will throw an EADDRNOTAVAIL error.
        // TODO: send RTM_DELADDR instead of Netd API.
        mNetd.interfaceDelAddress(mIfaceName,
                ((Inet6Address) privacyAddress.getAddress()).getHostAddress(),
                privacyAddress.getPrefixLength());
        mNetd.interfaceDelAddress(mIfaceName,
                ((Inet6Address) stableAddress.getAddress()).getHostAddress(),
                stableAddress.getPrefixLength());

        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningFailure(captor.capture());
        lp = captor.getValue();
        assertFalse(lp.hasGlobalIpv6Address());
        assertEquals(1, lp.getLinkAddresses().size()); // only link-local
    }

    @Test
    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
    public void testMaxDtimMultiplier_IPv6OnlyNetwork() throws Exception {
        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIPv4()
                .build();
        startIpClientProvisioning(config);

        verify(mCb, timeout(TEST_TIMEOUT_MS)).setMaxDtimMultiplier(
                IpClient.DEFAULT_BEFORE_IPV6_PROV_MAX_DTIM_MULTIPLIER);

        LinkProperties lp = doIpv6OnlyProvisioning();
        assertNotNull(lp);
        assertEquals(3, lp.getLinkAddresses().size()); // IPv6 privacy, stable privacy, link-local
        verify(mCb, timeout(TEST_TIMEOUT_MS)).setMaxDtimMultiplier(
                IpClient.DEFAULT_IPV6_ONLY_NETWORK_MAX_DTIM_MULTIPLIER);
    }

    @Test
    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
    public void testMaxDtimMultiplier_IPv6LinkLocalOnlyMode() throws Exception {
        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIPv4()
                .withIpv6LinkLocalOnly()
                .build();
        startIpClientProvisioning(config);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(any());
        // IPv6 DTIM grace period doesn't apply to IPv6 link-local only mode and the multiplier
        // has been initialized to DTIM_MULTIPLIER_RESET before starting provisioning, therefore,
        // the multiplier should not be updated neither.
        verify(mCb, never()).setMaxDtimMultiplier(
                IpClient.DEFAULT_BEFORE_IPV6_PROV_MAX_DTIM_MULTIPLIER);
        verify(mCb, never()).setMaxDtimMultiplier(DTIM_MULTIPLIER_RESET);
    }

    @Test
    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
    public void testMaxDtimMultiplier_IPv4OnlyNetwork() throws Exception {
        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
                false /* shouldReplyRapidCommitAck */,
                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
        verify(mCb, timeout(TEST_TIMEOUT_MS).times(1)).setMaxDtimMultiplier(
                IpClient.DEFAULT_IPV4_ONLY_NETWORK_MAX_DTIM_MULTIPLIER);
        // IPv6 DTIM grace period doesn't apply to IPv4-only networks.
        verify(mCb, never()).setMaxDtimMultiplier(
                IpClient.DEFAULT_BEFORE_IPV6_PROV_MAX_DTIM_MULTIPLIER);
    }

    private void runDualStackNetworkDtimMultiplierSetting(final InOrder inOrder) throws Exception {
        doDualStackProvisioning();
        inOrder.verify(mCb).setMaxDtimMultiplier(
                IpClient.DEFAULT_BEFORE_IPV6_PROV_MAX_DTIM_MULTIPLIER);
        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).setMaxDtimMultiplier(
                IpClient.DEFAULT_DUAL_STACK_MAX_DTIM_MULTIPLIER);
    }

    @Test
    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
    public void testMaxDtimMultiplier_DualStackNetwork() throws Exception {
        final InOrder inOrder = inOrder(mCb);
        runDualStackNetworkDtimMultiplierSetting(inOrder);
    }

    @Test
    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
    public void testMaxDtimMultiplier_MulticastLock() throws Exception {
        final InOrder inOrder = inOrder(mCb);
        runDualStackNetworkDtimMultiplierSetting(inOrder);

        // Simulate to hold the multicast lock by disabling the multicast filter.
        mIIpClient.setMulticastFilter(false);
        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).setMaxDtimMultiplier(
                IpClient.DEFAULT_MULTICAST_LOCK_MAX_DTIM_MULTIPLIER);

        // Simulate to disable the multicast lock again, then check the multiplier should be
        // changed to 2 (dual-stack setting)
        mIIpClient.setMulticastFilter(true);
        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).setMaxDtimMultiplier(
                IpClient.DEFAULT_DUAL_STACK_MAX_DTIM_MULTIPLIER);
    }

    @Test
    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
    public void testMaxDtimMultiplier_MulticastLockEnabled_StoppedState() throws Exception {
        // Simulate to hold the multicast lock by disabling the multicast filter at StoppedState,
        // verify no callback to be sent, start dual-stack provisioning and verify the multiplier
        // to be set to 1 (multicast lock setting) later.
        mIIpClient.setMulticastFilter(false);
        verify(mCb, after(10).never()).setMaxDtimMultiplier(
                IpClient.DEFAULT_MULTICAST_LOCK_MAX_DTIM_MULTIPLIER);

        doDualStackProvisioning();
        verify(mCb, times(1)).setMaxDtimMultiplier(
                IpClient.DEFAULT_MULTICAST_LOCK_MAX_DTIM_MULTIPLIER);
    }

    @Test
    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
    public void testMaxDtimMultiplier_resetMultiplier() throws Exception {
        final InOrder inOrder = inOrder(mCb);
        runDualStackNetworkDtimMultiplierSetting(inOrder);

        verify(mCb, never()).setMaxDtimMultiplier(DTIM_MULTIPLIER_RESET);

        // Stop IpClient and verify if the multiplier has been reset.
        mIIpClient.stop();
        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).setMaxDtimMultiplier(DTIM_MULTIPLIER_RESET);
    }

    private IaPrefixOption buildIaPrefixOption(final IpPrefix prefix, int preferred,
            int valid) {
        return new IaPrefixOption((short) IaPrefixOption.LENGTH, preferred, valid,
                (byte) prefix.getPrefixLength(), prefix.getRawAddress() /* prefix */);
    }

    private void handleDhcp6Packets(final IpPrefix prefix, boolean shouldReplyRapidCommit)
            throws Exception {
        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 4500 /* preferred */,
                7200 /* valid */);
        handleDhcp6Packets(Collections.singletonList(ipo), 3600 /* t1 */, 4500 /* t2 */,
                shouldReplyRapidCommit);
    }

    private void handleDhcp6Packets(final List<IaPrefixOption> ipos, int t1, int t2,
            boolean shouldReplyRapidCommit) throws Exception {
        ByteBuffer iapd;
        Dhcp6Packet packet;
        while ((packet = getNextDhcp6Packet()) != null) {
            final PrefixDelegation pd = new PrefixDelegation(packet.getIaid(), t1, t2, ipos);
            iapd = pd.build();
            if (packet instanceof Dhcp6SolicitPacket) {
                if (shouldReplyRapidCommit) {
                    mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
                            (Inet6Address) mClientIpAddress, true /* rapidCommit */));
                } else {
                    mPacketReader.sendResponse(buildDhcp6Advertise(packet, iapd.array(), mClientMac,
                            (Inet6Address) mClientIpAddress));
                }
            } else if (packet instanceof Dhcp6RequestPacket) {
                mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
                          (Inet6Address) mClientIpAddress, false /* rapidCommit */));
            } else {
                fail("invalid DHCPv6 Packet");
            }

            if ((packet instanceof Dhcp6RequestPacket) || shouldReplyRapidCommit) {
                return;
            }
        }
        fail("No DHCPv6 packet received on interface within timeout");
    }

    private static boolean hasIpv6LinkLocalAddress(final LinkProperties lp) {
        for (LinkAddress address : lp.getLinkAddresses()) {
            if (address.isIpv6() && address.getAddress().isLinkLocalAddress()) {
                return true;
            }
        }
        return false;
    }

    private void prepareDhcp6PdTest() throws Exception {
        final String dnsServer = "2001:4860:4860::64";
        final ByteBuffer rdnss = buildRdnssOption(3600, dnsServer);
        final ByteBuffer pio = buildPioOption(3600 /* valid */, 1800 /* preferred */,
                (byte) PIO_FLAG_ON_LINK, TEST_IPV6_PREFIX);
        final ByteBuffer slla = buildSllaOption();
        final ByteBuffer ra = buildRaPacket(pio, rdnss, slla);

        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIPv4()
                .build();
        startIpClientProvisioning(config);

        waitForRouterSolicitation();

        // Waiting for the IPv6 link-local address to appear, which ensures that the IPv6 stack
        // is enabled and can process Router Advertisements.
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(argThat(
                x -> hasIpv6LinkLocalAddress(x)
        ));
        mPacketReader.sendResponse(ra);
    }

    @ConnectivityDiagnosticsCollector.CollectTcpdumpOnFailure
    @Test
    @EnableFlags(Flags.FLAG_IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME)
    public void testDhcp6Pd() throws Exception {
        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
        prepareDhcp6PdTest();
        handleDhcp6Packets(prefix, true /* shouldReplyRapidCommit */);
        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
        final LinkProperties lp = captor.getValue();
        assertTrue(hasIpv6AddressPrefixedWith(lp, prefix));

        final long now = SystemClock.elapsedRealtime();
        long when = 0;
        for (LinkAddress la : lp.getLinkAddresses()) {
            if (la.getAddress().isLinkLocalAddress()) {
                assertLinkAddressPermanentLifetime(la);
            } else if (la.isGlobalPreferred()) {
                when = now + 4500 * 1000; // preferred=4500s
                assertLinkAddressDeprecationTime(la, when);
                when = now + 7200 * 1000; // valid=7200s
                assertLinkAddressExpirationTime(la, when);
            }
        }
    }

    @Test
    public void testDhcp6Pd_disableRapidCommit() throws Exception {
        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
        prepareDhcp6PdTest();
        handleDhcp6Packets(prefix, false /* shouldReplyRapidCommit */);
        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
        assertTrue(hasIpv6AddressPrefixedWith(captor.getValue(), prefix));
    }

    @Test
    public void testDhcp6Pd_longPrefixLength() throws Exception {
        prepareDhcp6PdTest();
        final IpPrefix prefix = new IpPrefix("2001:db8:1::/80");
        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 3600 /* preferred */,
                4000 /* valid */);
        handleDhcp6Packets(Collections.singletonList(ipo), 3600 /* t1 */, 4500 /* t2 */,
                true /* shouldReplyRapidCommit */);
        verify(mCb, never()).onProvisioningSuccess(any());
    }

    @Test
    public void testDhcp6Pd_shortPrefixLength() throws Exception {
        final IpPrefix prefix = new IpPrefix("2001:db8:1::/56");
        prepareDhcp6PdTest();
        handleDhcp6Packets(prefix, true /* shouldReplyRapidCommit */);
        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
        assertTrue(hasIpv6AddressPrefixedWith(captor.getValue(), prefix));
    }

    @Test
    public void testDhcp6Pd_T1GreaterThanT2() throws Exception {
        prepareDhcp6PdTest();
        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 3600 /* preferred */,
                4000 /* valid */);
        handleDhcp6Packets(Collections.singletonList(ipo), 4500 /* t1 */, 3600 /* t2 */,
                true /* shouldReplyRapidCommit */);
        verify(mCb, never()).onProvisioningSuccess(any());
    }

    @Test
    public void testDhcp6Pd_preferredLifetimeGreaterThanValidLifetime() throws Exception {
        prepareDhcp6PdTest();
        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 7200 /* preferred */,
                4500 /* valid */);
        handleDhcp6Packets(Collections.singletonList(ipo), 3600 /* t1 */, 4500 /* t2 */,
                true /* shouldReplyRapidCommit */);
        verify(mCb, never()).onProvisioningSuccess(any());
    }

    @Test
    public void testDhcp6Pd_preferredLifetimeLessThanT2() throws Exception {
        prepareDhcp6PdTest();
        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 3600 /* preferred */,
                4000 /* valid */);
        handleDhcp6Packets(Collections.singletonList(ipo), 3600 /* t1 */, 4500 /* t2 */,
                true /* shouldReplyRapidCommit */);
        verify(mCb, never()).onProvisioningSuccess(any());
    }

    private void runDhcp6PdNotStartInDualStackTest(final String prefix, final String dnsServer)
            throws Exception {
        final List<ByteBuffer> options = new ArrayList<>();
        if (prefix != null) {
            options.add(buildPioOption(3600, 1800, prefix));
        }
        if (dnsServer != null) {
            options.add(buildRdnssOption(3600, dnsServer));
        }
        options.add(buildSllaOption());
        final ByteBuffer ra = buildRaPacket(options.toArray(new ByteBuffer[options.size()]));

        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .build();
        setDhcpFeatures(true /* isRapidCommitEnabled */,
                false /* isDhcpIpConflictDetectEnabled */);
        startIpClientProvisioning(config);

        waitForRouterSolicitation();
        mPacketReader.sendResponse(ra);

        // Start IPv4 provisioning and wait until entire provisioning completes.
        handleDhcpPackets(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
                true /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU, null /* serverSentUrl */);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(any());
    }

    @Test
    public void testDhcp6Pd_notStartWithGlobalPio() throws Exception {
        runDhcp6PdNotStartInDualStackTest("2001:db8:1::/64" /* prefix */,
                "2001:4860:4860::64" /* dnsServer */);
        // Reply with a normal RA with global prefix and an off-link DNS for IPv6 provisioning,
        // DHCPv6 prefix delegation should not start.
        assertNull(getNextDhcp6Packet(PACKET_TIMEOUT_MS));
    }

    @Test
    public void testDhcp6Pd_notStartWithUlaPioAndDns() throws Exception {
        runDhcp6PdNotStartInDualStackTest("fd7c:9df8:7f39:dc89::/64" /* prefix */,
                "fd7c:9df8:7f39:dc89::1"  /* dnsServer */);
        // Reply with a normal RA even with ULA prefix and on-link ULA DNS for IPv6 provisioning,
        // DHCPv6 prefix delegation should not start.
        assertNull(getNextDhcp6Packet(PACKET_TIMEOUT_MS));
    }

    @Test
    public void testDhcp6Pd_notStartWithUlaPioAndOffLinkDns() throws Exception {
        runDhcp6PdNotStartInDualStackTest("fd7c:9df8:7f39:dc89::/64" /* prefix */,
                "2001:4860:4860::64"  /* dnsServer */);
        // Reply with a normal RA even with ULA prefix and off-link DNS for IPv6 provisioning,
        // DHCPv6 prefix delegation should not start.
        assertNull(getNextDhcp6Packet(PACKET_TIMEOUT_MS));
    }

    @Test
    public void testDhcp6Pd_startWithNoNonIpv6LinkLocalAddresses() throws Exception {
        runDhcp6PdNotStartInDualStackTest(null /* prefix */,
                "2001:4860:4860::64"  /* dnsServer */);
        // Reply with a normal RA with only RDNSS but no PIO for IPv6 provisioning,
        // DHCPv6 prefix delegation should start.
        final Dhcp6Packet packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS);
        assertTrue(packet instanceof Dhcp6SolicitPacket);
    }

    @Test
    public void testDhcp6Pd_dualstack() throws Exception {
        final String dnsServer = "2001:4860:4860::64";
        final ByteBuffer rdnss = buildRdnssOption(3600, dnsServer);
        final ByteBuffer ra = buildRaPacket(rdnss);

        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .build();
        setDhcpFeatures(true /* isRapidCommitEnabled */,
                false /* isDhcpIpConflictDetectEnabled */);
        startIpClientProvisioning(config);

        waitForRouterSolicitation();
        mPacketReader.sendResponse(ra);

        // Start IPv4 provisioning and wait until entire provisioning completes.
        handleDhcpPackets(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
                true /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU, null /* serverSentUrl */);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(any());

        // Start DHCPv6 Prefix Delegation.
        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
        handleDhcp6Packets(prefix, false /* shouldReplyRapidCommit */);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(argThat(
                x -> x.isIpv6Provisioned()
                        && hasIpv6AddressPrefixedWith(x, prefix)
                        && hasRouteTo(x, "2001:db8:1::/64", RTN_UNREACHABLE)
                        // IPv4 address, IPv6 link-local, two global delegated IPv6 addresses
                        && x.getLinkAddresses().size() == 4
        ));
    }

    @Test
    public void testDhcp6Pd_multiplePrefixesWithInvalidPrefix() throws Exception {
        final IpPrefix valid = new IpPrefix("2001:db8:1::/64");
        final IpPrefix invalid = new IpPrefix("2001:db8:2::/64"); // preferred lft > valid lft
        final IaPrefixOption validIpo = buildIaPrefixOption(valid, 4500 /* preferred */,
                7200 /* valid */);
        final IaPrefixOption invalidIpo = buildIaPrefixOption(invalid, 4500 /* preferred */,
                3000 /* valid */);

        prepareDhcp6PdTest();
        handleDhcp6Packets(Arrays.asList(invalidIpo, validIpo), 3600 /* t1 */, 4500 /* t2 */,
                true /* shouldReplyRapidCommit */);
        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
        final LinkProperties lp = captor.getValue();
        assertTrue(hasIpv6AddressPrefixedWith(lp, valid));
        assertFalse(hasIpv6AddressPrefixedWith(lp, invalid));
    }

    @Test
    public void testDhcp6Pd_multiplePrefixesWithPrefixValidLifetimeOfZero() throws Exception {
        final IpPrefix valid = new IpPrefix("2001:db8:1::/64");
        final IpPrefix invalid = new IpPrefix("2001:db8:2::/64"); // preferred/valid lft 0
        final IaPrefixOption validIpo = buildIaPrefixOption(valid, 4500 /* preferred */,
                7200 /* valid */);
        final IaPrefixOption invalidIpo = buildIaPrefixOption(invalid, 0 /* preferred */,
                0 /* valid */);

        prepareDhcp6PdTest();
        handleDhcp6Packets(Arrays.asList(invalidIpo, validIpo), 3600 /* t1 */, 4500 /* t2 */,
                true /* shouldReplyRapidCommit */);
        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
        final LinkProperties lp = captor.getValue();
        assertTrue(hasIpv6AddressPrefixedWith(lp, valid));
        assertFalse(hasIpv6AddressPrefixedWith(lp, invalid));
    }

    private void prepareDhcp6PdRenewTest() throws Exception {
        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
        prepareDhcp6PdTest();
        handleDhcp6Packets(prefix, true /* shouldReplyRapidCommit */);
        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
        assertTrue(hasIpv6AddressPrefixedWith(captor.getValue(), prefix));
    }

    @Test
    @SignatureRequiredTest(reason = "Need to mock the DHCP6 renew/rebind alarms")
    public void testDhcp6Pd_renewAndRebind() throws Exception {
        prepareDhcp6PdRenewTest();

        final InOrder inOrder = inOrder(mAlarm);
        final Handler handler = mDependencies.mDhcp6Client.getHandler();
        final OnAlarmListener renewAlarm = expectAlarmSet(inOrder, "RENEW", 3600, handler);
        final OnAlarmListener rebindAlarm = expectAlarmSet(inOrder, "REBIND", 4500, handler);

        handler.post(() -> renewAlarm.onAlarm());
        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);

        Dhcp6Packet packet = getNextDhcp6Packet();
        assertTrue(packet instanceof Dhcp6RenewPacket);

        handler.post(() -> rebindAlarm.onAlarm());
        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);

        packet = getNextDhcp6Packet();
        assertTrue(packet instanceof Dhcp6RebindPacket);
    }

    @SignatureRequiredTest(reason = "Need to mock the DHCP6 renew/rebind alarms")
    @Test
    public void testDhcp6Pd_prefixMismatchOnRenew_newPrefix() throws Exception {
        prepareDhcp6PdRenewTest();

        final InOrder inOrder = inOrder(mAlarm);
        final Handler handler = mDependencies.mDhcp6Client.getHandler();
        final OnAlarmListener renewAlarm = expectAlarmSet(inOrder, "RENEW", 3600, handler);

        handler.post(() -> renewAlarm.onAlarm());
        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);

        Dhcp6Packet packet = getNextDhcp6Packet();
        assertTrue(packet instanceof Dhcp6RenewPacket);

        // Reply with a new prefix apart of the requested one, per RFC8415#section-18.2.10.1
        // any new prefix should be added.
        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
        final IpPrefix prefix1 = new IpPrefix("2001:db8:2::/64");
        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 4500 /* preferred */,
                7200 /* valid */);
        final IaPrefixOption ipo1 = buildIaPrefixOption(prefix1, 5000 /* preferred */,
                6000 /* valid */);
        final PrefixDelegation pd = new PrefixDelegation(packet.getIaid(), 3600 /* t1 */,
                4500 /* t2 */, Arrays.asList(ipo, ipo1));
        final ByteBuffer iapd = pd.build();
        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
        verify(mCb, never()).onProvisioningFailure(any());
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(argThat(
                x -> x.isIpv6Provisioned()
                        && hasIpv6AddressPrefixedWith(x, prefix)
                        && hasIpv6AddressPrefixedWith(x, prefix1)
                        && hasRouteTo(x, "2001:db8:1::/64", RTN_UNREACHABLE)
                        && hasRouteTo(x, "2001:db8:2::/64", RTN_UNREACHABLE)
                        // IPv6 link-local, four global delegated IPv6 addresses
                        && x.getLinkAddresses().size() == 5
        ));
    }

    @SignatureRequiredTest(reason = "Need to mock the DHCP6 renew/rebind alarms")
    @Test
    public void testDhcp6Pd_prefixMismatchOnRenew_requestedPrefixAbsent() throws Exception {
        prepareDhcp6PdRenewTest();

        final InOrder inOrder = inOrder(mAlarm);
        final Handler handler = mDependencies.mDhcp6Client.getHandler();
        final OnAlarmListener renewAlarm = expectAlarmSet(inOrder, "RENEW", 3600, handler);

        handler.post(() -> renewAlarm.onAlarm());
        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);

        Dhcp6Packet packet = getNextDhcp6Packet();
        assertTrue(packet instanceof Dhcp6RenewPacket);

        // Reply with a new prefix but the requested one is absent, per RFC8415#section-18.2.10.1
        // the new prefix should be added and the absent prefix will expire in nature.
        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
        final IpPrefix prefix1 = new IpPrefix("2001:db8:2::/64");
        final IaPrefixOption ipo = buildIaPrefixOption(prefix1, 4500 /* preferred */,
                7200 /* valid */);
        final PrefixDelegation pd = new PrefixDelegation(packet.getIaid(), 3600 /* t1 */,
                4500 /* t2 */, Arrays.asList(ipo));
        final ByteBuffer iapd = pd.build();
        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
        verify(mCb, never()).onProvisioningFailure(any());
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(argThat(
                x -> x.isIpv6Provisioned()
                        && hasIpv6AddressPrefixedWith(x, prefix)
                        && hasIpv6AddressPrefixedWith(x, prefix1)
                        && hasRouteTo(x, "2001:db8:1::/64", RTN_UNREACHABLE)
                        && hasRouteTo(x, "2001:db8:2::/64", RTN_UNREACHABLE)
                        // IPv6 link-local, four global delegated IPv6 addresses
                        && x.getLinkAddresses().size() == 5
        ));
    }

    @SignatureRequiredTest(reason = "Need to mock the DHCP6 renew/rebind alarms")
    @Test
    public void testDhcp6Pd_prefixMismatchOnRenew_allPrefixesAbsent() throws Exception {
        prepareDhcp6PdRenewTest();

        final InOrder inOrder = inOrder(mAlarm);
        final Handler handler = mDependencies.mDhcp6Client.getHandler();
        final OnAlarmListener renewAlarm = expectAlarmSet(inOrder, "RENEW", 3600, handler);

        handler.post(() -> renewAlarm.onAlarm());
        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);

        Dhcp6Packet packet = getNextDhcp6Packet();
        assertTrue(packet instanceof Dhcp6RenewPacket);

        clearInvocations(mCb);

        // Reply with IA_PD but IA_Prefix is absent, client should still stay at the RenewState
        // and restransmit the Renew message, that should not result in any LinkProperties update.
        final PrefixDelegation pd = new PrefixDelegation(packet.getIaid(), 3600 /* t1 */,
                4500 /* t2 */, new ArrayList<IaPrefixOption>(0));
        final ByteBuffer iapd = pd.build();
        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
        verify(mCb, never()).onLinkPropertiesChange(any());
    }

    @SignatureRequiredTest(reason = "Need to mock the DHCP6 renew/rebind alarms")
    @Test
    public void testDhcp6Pd_renewInvalidPrefixes_zeroPreferredAndValidLifetime() throws Exception {
        prepareDhcp6PdRenewTest();

        final InOrder inOrder = inOrder(mAlarm);
        final Handler handler = mDependencies.mDhcp6Client.getHandler();
        final OnAlarmListener renewAlarm = expectAlarmSet(inOrder, "RENEW", 3600, handler);

        handler.post(() -> renewAlarm.onAlarm());
        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);

        Dhcp6Packet packet = getNextDhcp6Packet();
        assertTrue(packet instanceof Dhcp6RenewPacket);

        // Reply with the requested prefix with preferred/valid lifetime of 0.
        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
        final IpPrefix prefix1 = new IpPrefix("2001:db8:2::/64");
        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 0 /* preferred */,
                0 /* valid */);
        final IaPrefixOption ipo1 = buildIaPrefixOption(prefix1, 5000 /* preferred */,
                6000 /* valid */);
        final PrefixDelegation pd = new PrefixDelegation(packet.getIaid(), 3600 /* t1 */,
                4500 /* t2 */, Arrays.asList(ipo, ipo1));
        final ByteBuffer iapd = pd.build();
        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
        verify(mCb, never()).onProvisioningFailure(any());
        // IPv6 addresses derived from prefix with 0 preferred/valid lifetime should be deleted.
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(argThat(
                x -> x.isIpv6Provisioned()
                        && !hasIpv6AddressPrefixedWith(x, prefix)
                        && hasIpv6AddressPrefixedWith(x, prefix1)
                        && !hasRouteTo(x, "2001:db8:1::/64", RTN_UNREACHABLE)
                        && hasRouteTo(x, "2001:db8:2::/64", RTN_UNREACHABLE)
                        // IPv6 link-local, two global delegated IPv6 addresses with prefix1
                        && x.getLinkAddresses().size() == 3
        ));

        handler.post(() -> renewAlarm.onAlarm());
        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);

        packet = getNextDhcp6Packet();
        assertTrue(packet instanceof Dhcp6RenewPacket);
        final List<IaPrefixOption> renewIpos = packet.getPrefixDelegation().ipos;
        assertEquals(1, renewIpos.size()); // don't renew prefix 2001:db8:1::/64 with 0
                                           // preferred/valid lifetime
        assertEquals(prefix1, renewIpos.get(0).getIpPrefix());
    }

    @SignatureRequiredTest(reason = "Need to mock the DHCP6 renew/rebind alarms")
    @Test
    public void testDhcp6Pd_renewInvalidPrefixes_theSameT1T2ValidLifetime() throws Exception {
        prepareDhcp6PdRenewTest();

        final InOrder inOrder = inOrder(mAlarm);
        final Handler handler = mDependencies.mDhcp6Client.getHandler();
        final OnAlarmListener renewAlarm = expectAlarmSet(inOrder, "RENEW", 3600, handler);

        handler.post(() -> renewAlarm.onAlarm());
        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);

        Dhcp6Packet packet = getNextDhcp6Packet();
        assertTrue(packet instanceof Dhcp6RenewPacket);

        clearInvocations(mCb);

        // Reply with the requested prefix with the same t1/t2/lifetime.
        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 3600 /* preferred */,
                3600 /* valid */);
        final PrefixDelegation pd = new PrefixDelegation(packet.getIaid(), 3600 /* t1 */,
                3600 /* t2 */, Collections.singletonList(ipo));
        final ByteBuffer iapd = pd.build();
        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
        // The prefix doesn't change only the lifetime is updated, therefore, LinkProperties update
        // isn't expected.
        verify(mCb, never()).onProvisioningFailure(any());
        verify(mCb, never()).onLinkPropertiesChange(any());

        handler.post(() -> renewAlarm.onAlarm());
        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);

        packet = getNextDhcp6Packet(TEST_TIMEOUT_MS);
        assertNull(packet);
    }

    @ConnectivityDiagnosticsCollector.CollectTcpdumpOnFailure
    @Test
    public void testDhcp6Pd_multipleIaPrefixOptions() throws Exception {
        final InOrder inOrder = inOrder(mCb);
        final IpPrefix prefix1 = new IpPrefix("2001:db8:1::/64");
        final IpPrefix prefix2 = new IpPrefix("2400:db8:100::/64");
        final IpPrefix prefix3 = new IpPrefix("fd7c:9df8:7f39:dc89::/64");
        final IaPrefixOption ipo1 = buildIaPrefixOption(prefix1, 4500 /* preferred */,
                7200 /* valid */);
        final IaPrefixOption ipo2 = buildIaPrefixOption(prefix2, 5600 /* preferred */,
                6000 /* valid */);
        final IaPrefixOption ipo3 = buildIaPrefixOption(prefix3, 7200 /* preferred */,
                14400 /* valid */);
        prepareDhcp6PdTest();
        handleDhcp6Packets(Arrays.asList(ipo1, ipo2, ipo3), 3600 /* t1 */, 4500 /* t2 */,
                true /* shouldReplyRapidCommit */);

        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verifyWithTimeout(inOrder, mCb).onProvisioningSuccess(captor.capture());
        LinkProperties lp = captor.getValue();

        // Sometimes privacy address or route may appear later along with onLinkPropertiesChange
        // callback, in this case we wait a bit longer to see all of these properties appeared and
        // then verify if they are what we are looking for.
        if (lp.getLinkAddresses().size() < 5) { // 1 IPv6 link-local and 4 global IPv6 addresses
                                                // derived from prefix1 and prefix2
            final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>();
            verifyWithTimeout(inOrder, mCb).onLinkPropertiesChange(argThat(x -> {
                if (!x.isIpv6Provisioned()) return false;
                if (x.getLinkAddresses().size() != 5) return false;
                lpFuture.complete(x);
                return true;
            }));
            lp = lpFuture.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        }
        assertNotNull(lp);
        assertTrue(hasIpv6AddressPrefixedWith(lp, prefix1));
        assertTrue(hasIpv6AddressPrefixedWith(lp, prefix2));
        assertFalse(hasIpv6AddressPrefixedWith(lp, prefix3));
        assertTrue(hasRouteTo(lp, prefix1.toString(), RTN_UNREACHABLE));
        assertTrue(hasRouteTo(lp, prefix2.toString(), RTN_UNREACHABLE));
        assertFalse(hasRouteTo(lp, prefix3.toString(), RTN_UNREACHABLE));
    }

    private void runDhcp6PacketWithNoPrefixAvailStatusCodeTest(boolean shouldReplyWithAdvertise)
            throws Exception {
        prepareDhcp6PdTest();
        Dhcp6Packet packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS);
        assertTrue(packet instanceof Dhcp6SolicitPacket);

        final PrefixDelegation pd = new PrefixDelegation(packet.getIaid(), 0 /* t1 */, 0 /* t2 */,
                new ArrayList<IaPrefixOption>() /* ipos */, Dhcp6Packet.STATUS_NO_PREFIX_AVAIL);
        final ByteBuffer iapd = pd.build();
        if (shouldReplyWithAdvertise) {
            mPacketReader.sendResponse(buildDhcp6Advertise(packet, iapd.array(), mClientMac,
                    (Inet6Address) mClientIpAddress));
        } else {
            mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
                    (Inet6Address) mClientIpAddress, true /* rapidCommit */));
        }

        // Check if client will ignore Advertise or Reply for Rapid Commit Solicit and
        // retransmit Solicit.
        packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS);
        assertTrue(packet instanceof Dhcp6SolicitPacket);
    }

    @Test
    public void testDhcp6AdvertiseWithNoPrefixAvailStatusCode() throws Exception {
        // Advertise
        runDhcp6PacketWithNoPrefixAvailStatusCodeTest(true /* shouldReplyWithAdvertise */);
    }

    @Test
    public void testDhcp6ReplyForRapidCommitSolicitWithNoPrefixAvailStatusCode() throws Exception {
        // Reply
        runDhcp6PacketWithNoPrefixAvailStatusCodeTest(false /* shouldReplyWithAdvertise */);
    }

    @Test
    public void testDhcp6ReplyForRequestWithNoPrefixAvailStatusCode() throws Exception {
        prepareDhcp6PdTest();
        Dhcp6Packet packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS);
        assertTrue(packet instanceof Dhcp6SolicitPacket);

        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 4500 /* preferred */,
                7200 /* valid */);
        PrefixDelegation pd = new PrefixDelegation(packet.getIaid(), 1000 /* t1 */,
                2000 /* t2 */, Arrays.asList(ipo));
        ByteBuffer iapd = pd.build();
        mPacketReader.sendResponse(buildDhcp6Advertise(packet, iapd.array(), mClientMac,
                (Inet6Address) mClientIpAddress));

        packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS);
        assertTrue(packet instanceof Dhcp6RequestPacket);

        // Reply for Request with NoPrefixAvail status code. Not sure if this is reasonable in
        // practice, but Server can do everything it wants.
        pd = new PrefixDelegation(packet.getIaid(), 0 /* t1 */, 0 /* t2 */,
                new ArrayList<IaPrefixOption>() /* ipos */, Dhcp6Packet.STATUS_NO_PREFIX_AVAIL);
        iapd = pd.build();
        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
                (Inet6Address) mClientIpAddress, false /* rapidCommit */));

        // Check if client will ignore Reply for Request with NoPrefixAvail status code, and
        // rollback to SolicitState.
        packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS);
        assertTrue(packet instanceof Dhcp6SolicitPacket);
    }

    @Test
    @SignatureRequiredTest(reason = "Need to mock the DHCP6 renew/rebind alarms")
    public void testDhcp6ReplyForRenewWithNoPrefixAvailStatusCode() throws Exception {
        prepareDhcp6PdRenewTest();

        final InOrder inOrder = inOrder(mAlarm);
        final Handler handler = mDependencies.mDhcp6Client.getHandler();
        final OnAlarmListener renewAlarm = expectAlarmSet(inOrder, "RENEW", 3600, handler);

        handler.post(() -> renewAlarm.onAlarm());
        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);

        Dhcp6Packet packet = getNextDhcp6Packet();
        assertTrue(packet instanceof Dhcp6RenewPacket);

        // Reply with normal IA_PD.
        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 4500 /* preferred */,
                7200 /* valid */);
        PrefixDelegation pd = new PrefixDelegation(packet.getIaid(), 1000 /* t1 */,
                2000 /* t2 */, Arrays.asList(ipo));
        ByteBuffer iapd = pd.build();
        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);

        // Trigger another Renew message.
        handler.post(() -> renewAlarm.onAlarm());
        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);

        packet = getNextDhcp6Packet();
        assertTrue(packet instanceof Dhcp6RenewPacket);

        // Reply for Renew with NoPrefixAvail status code, check if client will retransmit the
        // Renew message.
        pd = new PrefixDelegation(packet.getIaid(), 3600 /* t1 */, 4500 /* t2 */,
                new ArrayList<IaPrefixOption>(0) /* ipos */, Dhcp6Packet.STATUS_NO_PREFIX_AVAIL);
        iapd = pd.build();
        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
                (Inet6Address) mClientIpAddress, false /* rapidCommit */));

        packet = getNextDhcp6Packet(TEST_WAIT_RENEW_REBIND_RETRANSMIT_MS);
        assertTrue(packet instanceof Dhcp6RenewPacket);
    }

    @Test
    @SignatureRequiredTest(reason = "Need to mock the DHCP6 renew/rebind alarms")
    public void testDhcp6ReplyForRebindWithNoPrefixAvailStatusCode() throws Exception {
        prepareDhcp6PdRenewTest();

        final InOrder inOrder = inOrder(mAlarm);
        final Handler handler = mDependencies.mDhcp6Client.getHandler();
        final OnAlarmListener renewAlarm = expectAlarmSet(inOrder, "RENEW", 3600, handler);
        final OnAlarmListener rebindAlarm = expectAlarmSet(inOrder, "REBIND", 4500, handler);

        handler.post(() -> renewAlarm.onAlarm());
        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);

        Dhcp6Packet packet = getNextDhcp6Packet();
        assertTrue(packet instanceof Dhcp6RenewPacket);

        handler.post(() -> rebindAlarm.onAlarm());
        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);

        packet = getNextDhcp6Packet();
        assertTrue(packet instanceof Dhcp6RebindPacket);

        // Reply with normal IA_PD.
        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 4500 /* preferred */,
                7200 /* valid */);
        PrefixDelegation pd = new PrefixDelegation(packet.getIaid(), 1000 /* t1 */,
                2000 /* t2 */, Arrays.asList(ipo));
        ByteBuffer iapd = pd.build();
        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);

        // Trigger another Rebind message.
        handler.post(() -> renewAlarm.onAlarm());
        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);

        packet = getNextDhcp6Packet();
        assertTrue(packet instanceof Dhcp6RenewPacket);

        handler.post(() -> rebindAlarm.onAlarm());
        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);

        packet = getNextDhcp6Packet();
        assertTrue(packet instanceof Dhcp6RebindPacket);

        // Reply for Rebind with NoPrefixAvail status code, check if client will retransmit the
        // Rebind message.
        pd = new PrefixDelegation(packet.getIaid(), 3600 /* t1 */,
                4500 /* t2 */, new ArrayList<IaPrefixOption>(0) /* ipos */,
                Dhcp6Packet.STATUS_NO_PREFIX_AVAIL);
        iapd = pd.build();
        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
                (Inet6Address) mClientIpAddress, false /* rapidCommit */));

        packet = getNextDhcp6Packet(TEST_WAIT_RENEW_REBIND_RETRANSMIT_MS);
        assertTrue(packet instanceof Dhcp6RebindPacket);
    }

    @Test
    @SignatureRequiredTest(reason = "InterfaceParams.getByName requires CAP_NET_ADMIN")
    public void testSendRtmDelAddressMethod() throws Exception {
        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIPv4()
                .build();
        startIpClientProvisioning(config);

        final LinkProperties lp = doIpv6OnlyProvisioning();
        assertNotNull(lp);
        assertEquals(3, lp.getLinkAddresses().size()); // IPv6 privacy, stable privacy, link-local

        clearInvocations(mCb);

        // Delete all global IPv6 addresses, then that will trigger onProvisioningFailure callback.
        final InterfaceParams params = InterfaceParams.getByName(mIfaceName);
        for (LinkAddress la : lp.getLinkAddresses()) {
            if (la.isGlobalPreferred()) {
                NetlinkUtils.sendRtmDelAddressRequest(params.index, (Inet6Address) la.getAddress(),
                        (short) la.getPrefixLength());
                verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(argThat(
                        x -> !x.getLinkAddresses().contains(la)
                ));
            }
        }
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningFailure(any());
    }

    @Test
    @SignatureRequiredTest(reason = "requires mocked netd to read/write IPv6 sysctl")
    public void testIpv6SysctlsRestAfterStoppingIpClient() throws Exception {
        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIPv4()
                .build();
        // dad_transmits has been set to 0 in disableIpv6ProvisioningDelays, re-enable
        // dad_transmits for testing, production code will restore all IPv6 sysctls at
        // StoppedState#enter anyway, read this parameter value after IpClient shutdown
        // to check if that's default value 1.
        mNetd.setProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "dad_transmits", "1");
        startIpClientProvisioning(config);
        verify(mNetd, timeout(TEST_TIMEOUT_MS)).interfaceSetEnableIPv6(mIfaceName, true);
        doIpv6OnlyProvisioning();

        // Shutdown IpClient and check if the IPv6 sysctls: accept_ra, accept_ra_defrtr and
        // dad_transmits have been reset to the default values.
        mIpc.shutdown();
        awaitIpClientShutdown();
        final int dadTransmits = Integer.parseUnsignedInt(
                mNetd.getProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "dad_transmits"));
        assertEquals(1, dadTransmits);
        final int acceptRa = Integer.parseUnsignedInt(
                mNetd.getProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "accept_ra"));
        assertEquals(2, acceptRa);
        final int acceptRaDefRtr = Integer.parseUnsignedInt(
                mNetd.getProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "accept_ra_defrtr"));
        assertEquals(1, acceptRaDefRtr);
    }

    private void runDhcpDomainSearchListOptionTest(final String domainName,
            final List<String> domainSearchList, final String expectedDomain) throws Exception {
        when(mResources.getBoolean(R.bool.config_dhcp_client_domain_search_list)).thenReturn(true);
        final ProvisioningConfiguration cfg = new ProvisioningConfiguration.Builder()
                .withoutIpReachabilityMonitor()
                .withoutIPv6()
                .withCreatorUid(TEST_DEVICE_OWNER_APP_UID)
                .build();

        startIpClientProvisioning(cfg);
        handleDhcpPackets(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
                false /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU,
                null /* captivePortalApiUrl */, null /* ipv6OnlyWaitTime */,
                domainName, domainSearchList);

        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
        final LinkProperties lp = captor.getValue();
        assertNotNull(lp);
        assertEquals(expectedDomain, lp.getDomains());
    }

    @Test
    @SignatureRequiredTest(reason = "requires mocked DevicePolicyManager")
    public void testDhcpDomainSearchListOption() throws Exception {
        final String domainName = "google.com";
        final List<String> searchList = List.of("suffix1.google.com", "suffix2.google.com");
        final String expectedDomain = "google.com suffix1.google.com suffix2.google.com";
        runDhcpDomainSearchListOptionTest(domainName, searchList, expectedDomain);
    }

    @Test
    @SignatureRequiredTest(reason = "requires mocked DevicePolicyManager")
    public void testDhcpDomainSearchListOption_invalidSuffix() throws Exception {
        final String domainName = "google.com";
        final List<String> searchList = List.of("google com");
        runDhcpDomainSearchListOptionTest(domainName, searchList, domainName /* expectedDomain */);
    }

    @Test
    @SignatureRequiredTest(reason = "requires mocked DevicePolicyManager")
    public void testDhcpDomainSearchListOption_onlySearchList() throws Exception {
        final List<String> searchList = List.of("google.com", "example.com");
        final String expectedDomain = "google.com example.com";
        runDhcpDomainSearchListOptionTest(null /* domainName */, searchList,
                expectedDomain);
    }

    private void assertLinkAddressDeprecationTime(final LinkAddress la, final long when) {
        assertTrue(la.getDeprecationTime() != LinkAddress.LIFETIME_UNKNOWN);
        // Allow +/- 2 seconds to prevent flaky tests
        assertTrue(la.getDeprecationTime() < when + TEST_LIFETIME_TOLERANCE_MS);
        assertTrue(la.getDeprecationTime() > when - TEST_LIFETIME_TOLERANCE_MS);
    }

    private void assertLinkAddressExpirationTime(final LinkAddress la, final long when) {
        assertTrue(la.getExpirationTime() != LinkAddress.LIFETIME_UNKNOWN);
        // Allow +/- 2 seconds to prevent flaky tests
        assertTrue(la.getExpirationTime() < when + TEST_LIFETIME_TOLERANCE_MS);
        assertTrue(la.getExpirationTime() > when - TEST_LIFETIME_TOLERANCE_MS);
    }

    private void assertLinkAddressPermanentLifetime(final LinkAddress la) {
        assertEquals(LinkAddress.LIFETIME_PERMANENT, la.getDeprecationTime());
        assertEquals(LinkAddress.LIFETIME_PERMANENT, la.getExpirationTime());
    }

    @Test
    @EnableFlags(Flags.FLAG_IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME)
    public void testPopulateLinkAddressLifetime() throws Exception {
        final LinkProperties lp = doDualStackProvisioning();
        final long now = SystemClock.elapsedRealtime();
        long when = 0;
        for (LinkAddress la : lp.getLinkAddresses()) {
            if (la.isIpv4()) {
                when = now + 3600 * 1000; // DHCP lease duration
                assertLinkAddressDeprecationTime(la, when);
                assertLinkAddressExpirationTime(la, when);
            } else if (la.isIpv6() && la.getAddress().isLinkLocalAddress()) {
                assertLinkAddressPermanentLifetime(la);
            } else if (la.isIpv6() && la.isGlobalPreferred()) {
                when = now + 1800 * 1000; // preferred=1800s
                assertLinkAddressDeprecationTime(la, when);
                when = now + 3600 * 1000; // valid=3600s
                assertLinkAddressExpirationTime(la, when);
            }
        }
    }

    @Test
    @EnableFlags(Flags.FLAG_IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME)
    public void testPopulateLinkAddressLifetime_infiniteLeaseDuration() throws Exception {
        final ProvisioningConfiguration cfg = new ProvisioningConfiguration.Builder()
                .withoutIPv6()
                .build();

        startIpClientProvisioning(cfg);
        handleDhcpPackets(true /* isSuccessLease */, DhcpPacket.INFINITE_LEASE,
                false /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU,
                null /* captivePortalApiUrl */, null /* ipv6OnlyWaitTime */,
                null /* domainName */, null /* domainSearchList */);

        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
        final LinkProperties lp = captor.getValue();
        assertNotNull(lp);
        for (LinkAddress la : lp.getLinkAddresses()) {
            if (la.isIpv4()) {
                assertLinkAddressPermanentLifetime(la);
            }
        }
    }

    @Test
    @EnableFlags(Flags.FLAG_IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME)
    public void testPopulateLinkAddressLifetime_minimalLeaseDuration() throws Exception {
        final ProvisioningConfiguration cfg = new ProvisioningConfiguration.Builder()
                .withoutIPv6()
                .build();

        startIpClientProvisioning(cfg);
        handleDhcpPackets(true /* isSuccessLease */, 59 /* lease duration */,
                false /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU,
                null /* captivePortalApiUrl */, null /* ipv6OnlyWaitTime */,
                null /* domainName */, null /* domainSearchList */);

        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
        final LinkProperties lp = captor.getValue();
        assertNotNull(lp);
        for (LinkAddress la : lp.getLinkAddresses()) {
            if (la.isIpv4()) {
                final long now = SystemClock.elapsedRealtime();
                final long when = now + 60 * 1000; // minimal lease duration
                assertLinkAddressDeprecationTime(la, when);
                assertLinkAddressExpirationTime(la, when);
            }
        }
    }

    @Test
    @EnableFlags(Flags.FLAG_IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME)
    public void testPopulateLinkAddressLifetime_onDhcpRenew() throws Exception {
        final ProvisioningConfiguration cfg = new ProvisioningConfiguration.Builder()
                .withoutIPv6()
                .build();
        setDeviceConfigProperty(CONFIG_MINIMUM_LEASE,  5/* default minimum lease */);
        startIpClientProvisioning(cfg);
        handleDhcpPackets(true /* isSuccessLease */, 4 /* lease duration */,
                false /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU,
                null /* captivePortalApiUrl */, null /* ipv6OnlyWaitTime */,
                null /* domainName */, null /* domainSearchList */);

        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(any());

        // Device sends ARP request for address resolution of default gateway first.
        final ArpPacket request = getNextArpPacket();
        assertArpRequest(request, SERVER_ADDR);
        sendArpReply(request.senderHwAddress.toByteArray() /* dst */, ROUTER_MAC_BYTES /* srcMac */,
                request.senderIp /* target IP */, SERVER_ADDR /* sender IP */);

        // Then client sends unicast DHCPREQUEST to extend the IPv4 address lifetime, and we reply
        // with DHCPACK to refresh the DHCP lease.
        final DhcpPacket packet = getNextDhcpPacket();
        assertTrue(packet instanceof DhcpRequestPacket);
        assertDhcpRequestForReacquire(packet);
        mPacketReader.sendResponse(buildDhcpAckPacket(packet, CLIENT_ADDR,
                TEST_LEASE_DURATION_S, (short) TEST_DEFAULT_MTU,
                false /* rapidCommit */, null /* captivePortalApiUrl */));

        // Once the IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION flag is enabled, the IP
        // lease will be refreshed as well as the link address lifetime by transiting to
        // ConfiguringInterfaceState, where IpClient sends a new RTM_NEWADDR message to kernel
        // to update the IPv4 address, therefore, we should never see provisioning failure any
        // more.
        verify(mCb, never()).onProvisioningFailure(any());
    }

    private void doDhcpHostnameSettingTest(int hostnameSetting,
            boolean isHostnameConfigurationEnabled, boolean expectSendHostname) throws Exception {
        final ProvisioningConfiguration cfg = new ProvisioningConfiguration.Builder()
                .withoutIPv6()
                .withHostnameSetting(hostnameSetting)
                .build();
        final String expectedHostname;
        final String expectedHostnameAfterTransliteration;
        if (mDependencies != null) {
            mDependencies.setHostnameConfiguration(isHostnameConfigurationEnabled,
                    TEST_HOST_NAME);
            expectedHostname = TEST_HOST_NAME;
            expectedHostnameAfterTransliteration = TEST_HOST_NAME_TRANSLITERATION;
        } else {
            expectedHostname = Settings.Global.getString(
                    InstrumentationRegistry.getInstrumentation().getContext().getContentResolver(),
                    Settings.Global.DEVICE_NAME);
            expectedHostnameAfterTransliteration = new HostnameTransliterator()
                    .transliterate(expectedHostname);
        }
        startIpClientProvisioning(cfg);

        // perform DHCP handshake and capture the packets sent from client such as
        // DHCPDISCOVER and DHCPREQUEST.
        final List<DhcpPacket> sentPackets = handleDhcpPackets(true /* isSuccessLease */,
                DhcpPacket.INFINITE_LEASE,
                false /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU,
                null /* captivePortalApiUrl */, null /* ipv6OnlyWaitTime */,
                null /* domainName */, null /* domainSearchList */);

        // check if the DHCP packet sent from the client takes a hostname option per different
        // configs. Do not consider the null hostname case.
        assertHostname(expectSendHostname, expectedHostname, expectedHostnameAfterTransliteration,
                sentPackets);
    }

    @Test
    @SignatureRequiredTest(reason = "need to mock setHostnameConfiguration")
    public void testHostname_hostnameSettingUnset_enableHostnameConfig() throws Exception {
        // If hostname setting is unset but legacy hostname overlay config is enabled,
        // we expect that the DHCP packet takes a hostname option.
        doDhcpHostnameSettingTest(IIpClient.HOSTNAME_SETTING_UNSET,
                true /* isHostnameConfigurationEnabled */, true /* expectSendHostname */);
    }

    @Test
    @SignatureRequiredTest(reason = "need to mock setHostnameConfiguration")
    public void testHostname_hostnameSettingUnset_disableHostnameConfig() throws Exception {
        // If hostname setting is unset and legacy hostname overlay config is disabled,
        // we expect that the DHCP packet doesn't take a hostname option.
        doDhcpHostnameSettingTest(IIpClient.HOSTNAME_SETTING_UNSET,
                false /* isHostnameConfigurationEnabled */, false /* expectSendHostname */);
    }

    @Test
    public void testHostname_hostnameSettingSend_enableHostnameConfig() throws Exception {
        // If hostname setting is set and legacy hostname overlay config is enabled,
        // we expect that the DHCP packet takes a hostname option.
        doDhcpHostnameSettingTest(IIpClient.HOSTNAME_SETTING_SEND,
                true /* isHostnameConfigurationEnabled */, true /* expectSendHostname */);
    }

    @Test
    public void testHostname_hostnameSettingSend_disableHostnameConfig() throws Exception {
        // If hostname setting is set and legacy hostname overlay config is disabled,
        // we still expect that the DHCP packet takes a hostname option.
        doDhcpHostnameSettingTest(IIpClient.HOSTNAME_SETTING_SEND,
                false /* isHostnameConfigurationEnabled */, true /* expectSendHostname */);
    }

    @Test
    public void testHostname_hostnameSettingNotSend_enableHostnameConfig() throws Exception {
        // If hostname setting is not send and even if legacy hostname overlay config is
        // enabled, we expect that the DHCP packet doesn't take a hostname option.
        doDhcpHostnameSettingTest(IIpClient.HOSTNAME_SETTING_DO_NOT_SEND,
                true /* isHostnameConfigurationEnabled */, false /* expectSendHostname */);
    }

    @Test
    public void testHostname_hostnameSettingNotSend_disableHostnameConfig() throws Exception {
        // If hostname setting is not send and even if legacy hostname overlay config is
        // disabled, we expect that the DHCP packet doesn't take a hostname option.
        doDhcpHostnameSettingTest(IIpClient.HOSTNAME_SETTING_DO_NOT_SEND,
                false /* isHostnameConfigurationEnabled */, false /* expectSendHostname */);
    }

    // Store the network event to database multiple times.
    private void storeNudFailureEvents(long when, long expiry, int times, int eventType) {
        for (int i = 0; i < times; i++) {
            storeNetworkEvent(TEST_CLUSTER, when, expiry, eventType);
            when += 60 * 1000; // event interval is 1min
            expiry += 60 * 1000; // expiry also delays 1min
        }
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = true)
    public void testIgnoreNudFailuresIfTooManyInPastDay() throws Exception {
        // // NUD failure event count exceeds daily threshold nor weekly.
        final long when = System.currentTimeMillis() - ONE_DAY_IN_MS / 2; // 12h ago
        final long expiry = when + ONE_WEEK_IN_MS;
        storeNudFailureEvents(when, expiry, 10, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM);

        runIpReachabilityMonitorProbeFailedTest();
        assertNeverNotifyNeighborLost();
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = false)
    public void testIgnoreNudFailuresIfTooManyInPastDay_flagOff() throws Exception {
        // NUD failure event count exceeds daily threshold nor weekly.
        final long when = System.currentTimeMillis() - ONE_DAY_IN_MS / 2; // 12h ago
        final long expiry = when + ONE_WEEK_IN_MS;
        storeNudFailureEvents(when, expiry, 19, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM);

        runIpReachabilityMonitorProbeFailedTest();
        assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */,
                NudEventType.NUD_POST_ROAMING_FAILED_CRITICAL);
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = true)
    public void testIgnoreNudFailuresIfTooManyInPastDay_notUpToThreshold()
            throws Exception {
        // NUD failure event count doesn't exceed either weekly threshold nor daily.
        final long when = System.currentTimeMillis() - ONE_DAY_IN_MS / 2; // 12h ago
        final long expiry = when + ONE_WEEK_IN_MS;
        storeNudFailureEvents(when, expiry, 9, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM);

        runIpReachabilityMonitorProbeFailedTest();
        assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */,
                NudEventType.NUD_POST_ROAMING_FAILED_CRITICAL);
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = true)
    public void testIgnoreNudFailuresIfTooManyInPastWeek() throws Exception {
        // NUD failure event count exceeds the weekly threshold, but not daily threshold in the past
        // day.
        long when = System.currentTimeMillis() - ONE_WEEK_IN_MS / 2; // half a week ago
        long expiry = when + ONE_WEEK_IN_MS;
        storeNudFailureEvents(when, expiry, 11, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM);

        when = System.currentTimeMillis() - ONE_DAY_IN_MS / 2; // 12h ago
        expiry = when + ONE_WEEK_IN_MS;
        storeNudFailureEvents(when, expiry, 9, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM);

        runIpReachabilityMonitorProbeFailedTest();
        assertNeverNotifyNeighborLost();
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = false)
    public void testIgnoreNudFailuresIfTooManyInPastWeek_flagOff() throws Exception {
        // NUD failure event count exceeds the weekly threshold, but not daily threshold in the past
        // day.
        long when = System.currentTimeMillis() - ONE_WEEK_IN_MS / 2; // half a week ago
        long expiry = when + ONE_WEEK_IN_MS;
        storeNudFailureEvents(when, expiry, 11, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM);

        when = System.currentTimeMillis() - ONE_DAY_IN_MS / 2; // 12h ago
        expiry = when + ONE_WEEK_IN_MS;
        storeNudFailureEvents(when, expiry, 9, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM);

        runIpReachabilityMonitorProbeFailedTest();
        assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */,
                NudEventType.NUD_POST_ROAMING_FAILED_CRITICAL);
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = true)
    public void testIgnoreNudFailuresIfTooManyInPastWeek_notUpToThreshold() throws Exception {
        // NUD failure event count doesn't exceed either weekly threshold nor daily.
        long when = System.currentTimeMillis() - ONE_WEEK_IN_MS / 2; // half a week ago
        long expiry = when + ONE_WEEK_IN_MS;
        storeNudFailureEvents(when, expiry, 10, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM);

        when = System.currentTimeMillis() - ONE_DAY_IN_MS / 2; // 12h ago
        expiry = when + ONE_WEEK_IN_MS;
        storeNudFailureEvents(when, expiry, 9, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM);

        runIpReachabilityMonitorProbeFailedTest();
        assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */,
                NudEventType.NUD_POST_ROAMING_FAILED_CRITICAL);
    }

    private void assertRetrievedNetworkEventCount(String cluster, int expectedCountInPastWeek,
            int expectedCountInPastDay, int expectedCountInPastSixHours) {
        final long now = System.currentTimeMillis();
        final long[] sinceTimes = new long[3];
        sinceTimes[0] = now - ONE_WEEK_IN_MS;
        sinceTimes[1] = now - ONE_DAY_IN_MS;
        sinceTimes[2] = now - SIX_HOURS_IN_MS;
        final int[] counts = getStoredNetworkEventCount(cluster, sinceTimes,
                NETWORK_EVENT_NUD_FAILURE_TYPES, TEST_TIMEOUT_MS);
        assertEquals(expectedCountInPastWeek, counts[0]);
        assertEquals(expectedCountInPastDay, counts[1]);
        assertEquals(expectedCountInPastSixHours, counts[2]);
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = true)
    public void testIgnoreNudFailuresIfTooManyInPastWeek_stopWritingEvent() throws Exception {
        long when = (long) (System.currentTimeMillis() - SIX_HOURS_IN_MS * 0.9);
        long expiry = when + ONE_WEEK_IN_MS;
        storeNudFailureEvents(when, expiry, 10, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC);

        // Expect that a NUD failure happens, but onReachabilityFailure callback won't be called due
        // to the experiment flag is enabled and this event won't be written to db because the
        // number of events has been up to the threshold, then the retrieved event count should
        // still be 10.
        runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER,
                ROUTER_LINK_LOCAL /* targetIp */,
                true /* shouldMakeNeighborReachableFirst */,
                false /* expectNeighborLost */);
        assertRetrievedNetworkEventCount(TEST_CLUSTER, 10 /* expectedCountInPastWeek */,
                10 /* expectedCountInPastDay */, 10 /* expectedCountInPastSixHours */);
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = true)
    public void testIgnoreNudFailuresStopWritingEvents() throws Exception {
        // Add enough failures that NUD failures are ignored.
        long when = (long) (System.currentTimeMillis() - SIX_HOURS_IN_MS * 1.1);
        long expiry = when + ONE_WEEK_IN_MS;
        storeNudFailureEvents(when, expiry, 10, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC);

        // Add enough recent failures to almost, but not quite reach the 6-hour threshold.
        when = (long) (System.currentTimeMillis() - SIX_HOURS_IN_MS * 0.1);
        expiry = when + ONE_WEEK_IN_MS;
        storeNudFailureEvents(when, expiry, 9, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC);

        prepareIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER,
                ROUTER_LINK_LOCAL, true /* shouldMakeNeighborReachableFirst */);

        // The first new failure is ignored and written to the database.
        // The total is 10 failures in the last 6 hours, and 20 failures
        // in the past week and day.
        sendPacketToPeer(ipv6Addr(IPV6_OFF_LINK_DNS_SERVER));
        expectAndDropMultipleNses(ROUTER_LINK_LOCAL, false /* expectNeighborLost */);
        assertRetrievedNetworkEventCount(TEST_CLUSTER, 20 /* expectedCountInPastWeek */,
                20 /* expectedCountInPastDay */, 10 /* expectedCountInPastSixHours */);

        // The second new failure is ignored, but not written.
        sendPacketToPeer(ipv6Addr(IPV6_ON_LINK_DNS_SERVER));
        expectAndDropMultipleNses(ipv6Addr(IPV6_ON_LINK_DNS_SERVER),
                false /* expectNeighborLost */);
        assertRetrievedNetworkEventCount(TEST_CLUSTER, 20 /* expectedCountInPastWeek */,
                20 /* expectedCountInPastDay */, 10 /* expectedCountInPastSixHours */);
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = false)
    public void testIgnoreNudFailuresIfTooManyInPastWeek_stopWritingEvent_flagOff()
            throws Exception {
        long when = (long) (System.currentTimeMillis() - SIX_HOURS_IN_MS * 0.9);
        long expiry = when + ONE_WEEK_IN_MS;
        storeNudFailureEvents(when, expiry, 8, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC);

        runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER,
                ROUTER_LINK_LOCAL /* targetIp */,
                true /* shouldMakeNeighborReachableFirst */,
                true /* expectNeighborLost */);

        // Although the total NUD failure events count in the past 6 hours hasn't been up to the
        // threshold, however, the experiment flag is disabled, therefore, the new NUD failure
        // event will not be written to db, then the retrieved event count should still be 8 rather
        // than 9.
        assertRetrievedNetworkEventCount(TEST_CLUSTER, 8 /* expectedCountInPastWeek */,
                8 /* expectedCountInPastDay */, 8 /* expectedCountInPastSixHours */);
    }

    @Test
    @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = true)
    public void testIgnoreNudFailuresIfTooManyInPastWeek_stopWritingEvent_notUpToThreshold()
            throws Exception {
        long when = (long) (System.currentTimeMillis() - SIX_HOURS_IN_MS * 0.9);
        long expiry = when + ONE_WEEK_IN_MS;
        storeNudFailureEvents(when, expiry, 9, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC);

        runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER,
                ROUTER_LINK_LOCAL /* targetIp */,
                true /* shouldMakeNeighborReachableFirst */,
                true /* expectNeighborLost */);
        assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */,
                NudEventType.NUD_ORGANIC_FAILED_CRITICAL);

        // Given that total NUD failure event counts in the past 6 hours doesn't exceed the
        // threshold yet, the new NUD failure event will be written to db, then the retrieved
        // event count should be 10.
        assertRetrievedNetworkEventCount(TEST_CLUSTER, 10 /* expectedCountInPastWeek */,
                10 /* expectedCountInPastDay */, 10 /* expectedCountInPastSixHours */);
    }

    private void prepareDhcp6PrefixDelegationPreferredFlagTests(byte flags, boolean hasUlaPio)
            throws Exception {
        // DHCPv6 prefix delegation preferred flag relevant test cases require the kernel to support
        // "ra_honor_pio_pflag" sysctl, which lands since 6.12 kernel version.
        final String ra_honor_pio_flag =
                "/proc/sys/net/ipv6/conf/" + mIfaceName + "/ra_honor_pio_pflag";
        assumeTrue(new File(ra_honor_pio_flag).exists());

        final List<ByteBuffer> options = new ArrayList<>();
        final ByteBuffer pio =
                buildPioOption(3600 /* valid */, 1800 /* preferred */, flags, TEST_IPV6_PREFIX);
        final ByteBuffer ulaPio =
                buildPioOption(200 /* valid */, 100 /* preferred */, TEST_IPV6_ULA_PREFIX);
        final ByteBuffer rdnss = buildRdnssOption(3600, IPV6_OFF_LINK_DNS_SERVER);
        options.add(pio);
        if (hasUlaPio) options.add(ulaPio);
        options.add(rdnss);
        final ByteBuffer ra = buildRaPacket(options.toArray(new ByteBuffer[options.size()]));

        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIPv4()
                .build();
        startIpClientProvisioning(config);

        waitForRouterSolicitation();
        mPacketReader.sendResponse(ra);
    }

    @Test
    @Flag(name = IPCLIENT_DHCPV6_PD_PREFERRED_FLAG_VERSION, enabled = true)
    public void testDhcp6PrefixDelegationPreferred() throws Exception {
        prepareDhcp6PrefixDelegationPreferredFlagTests(TEST_PIO_FLAGS_P_SET, false /* hasUlaPio */);

        // Verify that DHCPv6 Prefix Delegation should be used for IPv6 provisioning when P bit
        // is set in the PIO, and device should ignore the A bit and haven't any SLAAC address
        // derived from that on-link prefix.
        final IpPrefix delegatedPrefix = new IpPrefix(TEST_DHCP6_DELEGATED_PREFIX);
        handleDhcp6Packets(delegatedPrefix, true /* shouldReplyRapidCommit */);
        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
        final LinkProperties lp = captor.getValue();
        assertTrue(hasIpv6AddressPrefixedWith(lp, delegatedPrefix));
        assertFalse(hasIpv6AddressPrefixedWith(lp, new IpPrefix(TEST_IPV6_PREFIX)));
    }

    @Test
    @Flag(name = IPCLIENT_DHCPV6_PD_PREFERRED_FLAG_VERSION, enabled = true)
    public void testDhcp6PrefixDelegationPreferred_withoutPFlag() throws Exception {
        prepareDhcp6PrefixDelegationPreferredFlagTests(TEST_PIO_FLAGS_P_UNSET,
                false /* hasUlaPio */);

        // Verify that DHCPv6 Prefix Delegation should not be used for IPv6 provisioning when P bit
        // is not set in the PIO, and device should do SLAAC based on the on-link prefix in PIO.
        assertNull(getNextDhcp6Packet(PACKET_TIMEOUT_MS));
        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
        final LinkProperties lp = captor.getValue();
        assertTrue(hasIpv6AddressPrefixedWith(lp, new IpPrefix(TEST_IPV6_PREFIX)));
        assertFalse(hasIpv6AddressPrefixedWith(lp, new IpPrefix(TEST_DHCP6_DELEGATED_PREFIX)));
    }

    @Test
    @Flag(name = IPCLIENT_DHCPV6_PD_PREFERRED_FLAG_VERSION, enabled = true)
    public void testDhcp6PrefixDelegationPreferred_multiplePiosWithPFlag() throws Exception {
        prepareDhcp6PrefixDelegationPreferredFlagTests(TEST_PIO_FLAGS_P_SET, true /* hasUlaPio */);

        // Verify that DHCPv6 Prefix Delegation should be used for IPv6 provisioning when the
        // P bit is set in the PIO, apart of that, RA also includes a PIO with ULA prefix, so
        // the device ignore the A bit in the PIO and have the ULA address based on the ULA prefix.
        final IpPrefix delegatedPrefix = new IpPrefix(TEST_DHCP6_DELEGATED_PREFIX);
        handleDhcp6Packets(delegatedPrefix, true /* shouldReplyRapidCommit */);
        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
        final LinkProperties lp = captor.getValue();
        assertTrue(hasIpv6AddressPrefixedWith(lp, delegatedPrefix));
        assertTrue(hasIpv6AddressPrefixedWith(lp, new IpPrefix(TEST_IPV6_ULA_PREFIX)));
        assertFalse(hasIpv6AddressPrefixedWith(lp, new IpPrefix(TEST_IPV6_PREFIX)));
    }

    @Test
    @Flag(name = IPCLIENT_DHCPV6_PD_PREFERRED_FLAG_VERSION, enabled = true)
    public void testDhcp6PrefixDelegationPreferred_withPFlag_preferredLifetimeBecomesZero()
            throws Exception {
        prepareDhcp6PrefixDelegationPreferredFlagTests(TEST_PIO_FLAGS_P_SET, true /* hasUlaPio */);

        final IpPrefix delegatedPrefix = new IpPrefix(TEST_DHCP6_DELEGATED_PREFIX);
        handleDhcp6Packets(delegatedPrefix, true /* shouldReplyRapidCommit */);
        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
        final LinkProperties lp = captor.getValue();
        assertTrue(hasIpv6AddressPrefixedWith(lp, delegatedPrefix));
        assertFalse(hasIpv6AddressPrefixedWith(lp, new IpPrefix(TEST_IPV6_PREFIX)));

        clearInvocations(mCb);

        // Send another PIO with P flag but 0 preferred lifetime in the RA, this will result in the
        // prefix being removed from the list, but the lifetimes of any prefixes already obtained
        // via DHCPv6 are unaffected, i.e. there should be no any change on the LinkProperties.
        final ByteBuffer pio = buildPioOption(3600 /* valid */, 0 /* preferred */,
                TEST_PIO_FLAGS_P_SET, TEST_IPV6_PREFIX);
        sendRouterAdvertisement(false /* waitForRs*/, (short) 1800 /* router lifetime */, pio);
        verify(mCb, never()).onLinkPropertiesChange(any());
    }
}
