| /* |
| * 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.dhcp6.Dhcp6Packet.PrefixDelegation; |
| 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.DEFAULT_ACCEPT_RA_MIN_LFT; |
| import static android.net.ip.IpClient.DEFAULT_APF_COUNTER_POLLING_INTERVAL_SECS; |
| 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_POPULATE_LINK_ADDRESS_LIFETIME_VERSION; |
| import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION; |
| import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_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.doThrow; |
| 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.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.Dhcp6Client; |
| import android.net.dhcp6.Dhcp6Packet; |
| import android.net.dhcp6.Dhcp6Packet.PrefixDelegation; |
| import android.net.dhcp6.Dhcp6RebindPacket; |
| import android.net.dhcp6.Dhcp6RenewPacket; |
| import android.net.dhcp6.Dhcp6RequestPacket; |
| import android.net.dhcp6.Dhcp6SolicitPacket; |
| import android.net.ipmemorystore.NetworkAttributes; |
| import android.net.ipmemorystore.OnNetworkAttributesRetrievedListener; |
| 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.os.SystemProperties; |
| import android.provider.Settings; |
| import android.stats.connectivity.NudEventType; |
| import android.system.ErrnoException; |
| import android.system.Os; |
| import android.util.Log; |
| |
| 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.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.apishim.ConstantsShim; |
| import com.android.networkstack.apishim.common.ShimUtils; |
| import com.android.networkstack.ipmemorystore.IpMemoryStoreService; |
| 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.DevSdkIgnoreRule; |
| import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; |
| import com.android.testutils.HandlerUtils; |
| import com.android.testutils.TapPacketReader; |
| 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; |
| private 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; |
| |
| @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; |
| @Spy private INetd mNetd; |
| |
| protected IpClient mIpc; |
| protected Dependencies mDependencies; |
| |
| /***** END signature required test members *****/ |
| |
| protected IIpClientCallbacks mCb; |
| private IIpClient mIIpClient; |
| private String mIfaceName; |
| private HandlerThread mPacketReaderThread; |
| private Handler mHandler; |
| private TapPacketReader 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_TRANSACTION_ID_OFFSET = DHCP_HEADER_OFFSET + 4; |
| 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 IPV6_OFF_LINK_DNS_SERVER = "2001:4860:4860::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 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[] TEST_HOTSPOT_OUI = new byte[] { |
| (byte) 0x00, (byte) 0x17, (byte) 0xF2 |
| }; |
| 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_SSID = "0001docomo"; |
| 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, Dhcp6Client.Dependencies deps) { |
| mDhcp6Client = Dhcp6Client.makeDhcp6Client(context, controller, ifParams, 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); |
| } |
| } |
| |
| @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 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 DHCPv6 Prefix Delegation. |
| setFeatureEnabled(NetworkStackUtils.IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION, |
| true /* isDhcp6PrefixDelegationEnabled */); |
| |
| // 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 }); |
| |
| setDeviceConfigProperty(IpClient.CONFIG_MIN_RDNSS_LIFETIME, 67); |
| 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, DEFAULT_ACCEPT_RA_MIN_LFT); |
| |
| // Set the polling interval to update APF data snapshot. |
| setDeviceConfigProperty(CONFIG_APF_COUNTER_POLLING_INTERVAL_SECS, |
| DEFAULT_APF_COUNTER_POLLING_INTERVAL_SECS); |
| } |
| |
| 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 TapPacketReader(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(); |
| } |
| } |
| |
| 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 packetContainsExpectedField(final byte[] packet, final int offset, |
| final byte[] expected) { |
| if (packet.length < offset + expected.length) return false; |
| for (int i = 0; i < expected.length; ++i) { |
| if (packet[offset + i] != expected[i]) return false; |
| } |
| return true; |
| } |
| |
| 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 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) { |
| assertNoHostname(packet.getHostname()); |
| } else { |
| assertEquals(hostnameAfterTransliteration, packet.getHostname()); |
| } |
| } |
| } |
| |
| private void assertNoHostname(String hostname) { |
| if (ShimUtils.isAtLeastR()) { |
| assertNull(hostname); |
| } else { |
| // Until Q, if no hostname is set, the device falls back to the hostname set via |
| // system property, to avoid breaking Q devices already launched with that setup. |
| assertEquals(SystemProperties.get("net.hostname"), hostname); |
| } |
| } |
| |
| // 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. |
| mNetd.interfaceSetMtu(mIfaceName, 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(mNetd, never()).interfaceSetMtu(mIfaceName, TEST_DEFAULT_MTU); |
| } else { |
| // Verify that MTU indeed has been restored or not. |
| verify(mNetd, times(shouldChangeMtu ? 1 : 0)) |
| .interfaceSetMtu(mIfaceName, 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) { |
| int arpPacketCount = 0; |
| 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 @IgnoreUpTo(Build.VERSION_CODES.Q) |
| 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 @IgnoreUpTo(Build.VERSION_CODES.Q) |
| public void testDhcpServerInLinkProperties() throws Exception { |
| assumeTrue(ConstantsShim.VERSION > Build.VERSION_CODES.Q); |
| |
| 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_WithException() throws Exception { |
| doThrow(new RemoteException("NetdNativeService::interfaceSetMtu")).when(mNetd) |
| .interfaceSetMtu(mIfaceName, TEST_DEFAULT_MTU); |
| |
| doRestoreInitialMtuTest(true /* shouldChangeMtu */, false /* shouldRemoveTestInterface */); |
| assertEquals(NetworkInterface.getByName(mIfaceName).getMTU(), TEST_MIN_MTU); |
| } |
| |
| @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. |
| mNetd.interfaceSetMtu(mIfaceName, 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 sendRouterAdvertisement(boolean waitForRs, short lifetime, int valid, |
| int preferred) throws Exception { |
| final ByteBuffer pio = buildPioOption(valid, preferred, "2001:db8:1::/64"); |
| 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 PrefixInformationOption.build(new IpPrefix(prefixString), |
| (byte) (PIO_FLAG_ON_LINK | PIO_FLAG_AUTONOMOUS), 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 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 pio = buildPioOption(3600, 1800, "2001:db8:1::/64"); |
| final ByteBuffer rdnss = buildRdnssOption(3600, IPV6_OFF_LINK_DNS_SERVER); |
| final ByteBuffer slla = buildSllaOption(); |
| final ByteBuffer ra = buildRaPacket(pio, rdnss, slla); |
| |
| return doIpv6OnlyProvisioning(inOrder, ra); |
| } |
| |
| private LinkProperties doIpv6OnlyProvisioning(InOrder inOrder, ByteBuffer ra) throws Exception { |
| waitForRouterSolicitation(); |
| 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_MIN_RDNSS_LIFETIME 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(68, 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 pio = buildPioOption(600, 300, "2001:db8:1::/64"); |
| // put an IPv6 link-local DNS server |
| final ByteBuffer rdnss = buildRdnssOption(600, ROUTER_LINK_LOCAL.getHostAddress()); |
| // put SLLA option to avoid address resolution for "fe80::1" |
| final ByteBuffer slla = buildSllaOption(); |
| final ByteBuffer ra = buildRaPacket(pio, rdnss, slla); |
| |
| 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 @IgnoreUpTo(Build.VERSION_CODES.Q) |
| @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required") |
| public void testPref64Option() throws Exception { |
| assumeTrue(ConstantsShim.VERSION > Build.VERSION_CODES.Q); |
| |
| 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. |
| // Can't verify the URL up to Q as there is no such attribute in LinkProperties. |
| if (!ShimUtils.isAtLeastR()) return null; |
| 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 assertDhcpResultsParcelable(final DhcpResultsParcelable lease) { |
| 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); |
| assertEquals(TEST_LEASE_DURATION_S, lease.leaseDuration); |
| } |
| |
| 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(); |
| assertDhcpResultsParcelable(lease); |
| |
| 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 pio = buildPioOption(3600, 1800, "2001:db8:1::/64"); |
| final ByteBuffer rdnss = buildRdnssOption(3600, IPV6_OFF_LINK_DNS_SERVER); |
| final ByteBuffer slla = buildSllaOption(); |
| final ByteBuffer ra = buildRaPacket(pio, rdnss, slla); |
| |
| return performDualStackProvisioning(ra, dnsServer); |
| } |
| |
| private LinkProperties performDualStackProvisioning(final ByteBuffer ra, |
| final InetAddress dnsServer) throws Exception { |
| final InOrder inOrder = inOrder(mCb); |
| 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 isGratuitousArpNaRoamingEnabled, |
| boolean hasIpv4, boolean hasIpv6) throws Exception { |
| final Layer2Information layer2Info = new Layer2Information(TEST_L2KEY, TEST_CLUSTER, |
| MacAddress.fromString(TEST_DEFAULT_BSSID)); |
| final ScanResultInfo scanResultInfo = |
| makeScanResultInfo(TEST_DEFAULT_SSID, TEST_DEFAULT_BSSID); |
| final ProvisioningConfiguration.Builder prov = new ProvisioningConfiguration.Builder() |
| .withoutIpReachabilityMonitor() |
| .withLayer2Information(layer2Info) |
| .withScanResultInfo(scanResultInfo) |
| .withDisplayName("ssid"); |
| if (!hasIpv4) prov.withoutIPv4(); |
| if (!hasIpv6) prov.withoutIPv6(); |
| |
| // Enable rapid commit to accelerate DHCP handshake to shorten test duration, |
| // not strictly necessary. |
| setDhcpFeatures(true /* isRapidCommitEnabled */, |
| false /* isDhcpIpConflictDetectEnabled */); |
| |
| if (isGratuitousArpNaRoamingEnabled) { |
| setFeatureEnabled(NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION, true); |
| } else { |
| setFeatureEnabled(NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION, false); |
| } |
| 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 /* isGratuitousArpNaRoamingEnabled */, |
| 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_disableExpFlag() throws Exception { |
| startGratuitousArpAndNaAfterRoamingTest(false /* isGratuitousArpNaRoamingEnabled */, |
| true /* hasIpv4 */, true /* hasIpv6 */); |
| performDualStackProvisioning(); |
| forceLayer2Roaming(); |
| |
| final List<ArpPacket> arpList = new ArrayList<>(); |
| final List<NeighborAdvertisement> naList = new ArrayList<>(); |
| waitForGratuitousArpAndNaPacket(arpList, naList); |
| assertEquals(2, naList.size()); // NAs sent due to RFC9131 implement, not from roam |
| assertEquals(0, arpList.size()); |
| } |
| |
| @Test |
| public void testGratuitousArpAndNaAfterRoaming_IPv6OnlyNetwork() throws Exception { |
| startGratuitousArpAndNaAfterRoamingTest(true /* isGratuitousArpNaRoamingEnabled */, |
| 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 /* isGratuitousArpNaRoamingEnabled */, |
| 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); |
| assertTrue(ns.ipv6Hdr.srcIp.isLinkLocalAddress()); |
| assertEquals(ICMPV6_NEIGHBOR_SOLICITATION, ns.icmpv6Hdr.type); |
| assertEquals(0, ns.icmpv6Hdr.code); |
| assertEquals(0, ns.nsHdr.reserved); |
| assertEquals(target, ns.nsHdr.target); |
| assertEquals(ns.slla.linkLayerAddress, ns.ethHdr.srcMac); |
| } |
| |
| private void assertUnicastNeighborSolicitation(final NeighborSolicitation ns, |
| final MacAddress dstMac, final Inet6Address dstIp, final Inet6Address target) { |
| assertEquals(dstMac, ns.ethHdr.dstMac); |
| assertEquals(dstIp, ns.ipv6Hdr.dstIp); |
| assertNeighborSolicitation(ns, target); |
| } |
| |
| private void 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 List<NeighborSolicitation> waitForMultipleNeighborSolicitations() throws Exception { |
| NeighborSolicitation ns; |
| final List<NeighborSolicitation> nsList = new ArrayList<NeighborSolicitation>(); |
| 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()) { |
| nsList.add(ns); |
| } |
| } |
| 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()); |
| } |
| |
| private void prepareIpReachabilityMonitorTest(boolean isMulticastResolicitEnabled) |
| throws Exception { |
| final ScanResultInfo info = makeScanResultInfo(TEST_DEFAULT_SSID, TEST_DEFAULT_BSSID); |
| ProvisioningConfiguration config = new ProvisioningConfiguration.Builder() |
| .withLayer2Information(new Layer2Information(TEST_L2KEY, TEST_CLUSTER, |
| MacAddress.fromString(TEST_DEFAULT_BSSID))) |
| .withScanResultInfo(info) |
| .withDisplayName(TEST_DEFAULT_SSID) |
| .withoutIPv4() |
| .build(); |
| setFeatureEnabled(NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION, |
| isMulticastResolicitEnabled); |
| startIpClientProvisioning(config); |
| verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true); |
| doIpv6OnlyProvisioning(); |
| |
| // Simulate the roaming. |
| forceLayer2Roaming(); |
| } |
| |
| private void runIpReachabilityMonitorProbeFailedTest() throws Exception { |
| prepareIpReachabilityMonitorTest(); |
| |
| final List<NeighborSolicitation> nsList = waitForMultipleNeighborSolicitations(); |
| final int expectedNudSolicitNum = readNudSolicitNumPostRoamingFromResource(); |
| assertEquals(expectedNudSolicitNum, nsList.size()); |
| 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 List<NeighborSolicitation> nsList = waitForMultipleNeighborSolicitations(); |
| final int expectedNudSolicitNum = readNudSolicitNumPostRoamingFromResource(); |
| int expectedSize = expectedNudSolicitNum + NUD_MCAST_RESOLICIT_NUM; |
| assertEquals(expectedSize, nsList.size()); |
| 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 void prepareIpReachabilityMonitorIpv4AddressResolutionTest() throws Exception { |
| 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 DNS server to trigger address resolution process for IPv4 |
| // on-link DNS server or default router. |
| final Random random = new Random(); |
| final byte[] data = new byte[100]; |
| random.nextBytes(data); |
| sendUdpPacketToNetwork(mNetworkAgent.getNetwork(), SERVER_ADDR, 1234 /* port */, data); |
| } |
| |
| private void doTestIpReachabilityMonitor_replyBroadcastArpRequestWithDiffMacAddresses( |
| boolean disconnect) throws Exception { |
| prepareIpReachabilityMonitorIpv4AddressResolutionTest(); |
| |
| // 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(1500); |
| |
| // 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(); |
| |
| 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(); |
| |
| ArpPacket packet; |
| while ((packet = getNextArpPacket(TEST_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); |
| } |
| |
| private void prepareIpReachabilityMonitorAddressResolutionTest(final String dnsServer, |
| final Inet6Address targetIp) throws Exception { |
| 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() |
| .build(); |
| startIpClientProvisioning(config); |
| verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true); |
| |
| final List<ByteBuffer> options = new ArrayList<ByteBuffer>(); |
| options.add(buildPioOption(3600, 1800, "2001:db8:1::/64")); // PIO |
| options.add(buildRdnssOption(3600, dnsServer)); // RDNSS |
| // If target IP of address resolution is default router's IPv6 link-local address, |
| // then we should not take SLLA option in RA. |
| if (!targetIp.equals(ROUTER_LINK_LOCAL)) { |
| options.add(buildSllaOption()); // SLLA |
| } |
| final ByteBuffer ra = buildRaPacket(options.toArray(new ByteBuffer[options.size()])); |
| final Inet6Address dnsServerIp = ipv6Addr(dnsServer); |
| final LinkProperties lp = performDualStackProvisioning(ra, dnsServerIp); |
| runAsShell(MANAGE_TEST_NETWORKS, () -> createTestNetworkAgentAndRegister(lp)); |
| |
| // Send a UDP packet to IPv6 DNS server to trigger address resolution process for IPv6 |
| // on-link DNS server or default router(if the target is default router, we should pass |
| // in an IPv6 off-link DNS server such as 2001:db8:4860:4860::64). |
| final Random random = new Random(); |
| final byte[] data = new byte[100]; |
| random.nextBytes(data); |
| sendUdpPacketToNetwork(mNetworkAgent.getNetwork(), dnsServerIp, 1234 /* port */, data); |
| } |
| |
| private void runIpReachabilityMonitorAddressResolutionTest(final String dnsServer, |
| final Inet6Address targetIp, |
| final boolean expectNeighborLost) throws Exception { |
| prepareIpReachabilityMonitorAddressResolutionTest(dnsServer, targetIp); |
| |
| // Wait for the multicast NSes but never respond to them, that results in the on-link |
| // DNS gets lost and onReachabilityLost callback will be invoked. |
| final List<NeighborSolicitation> nsList = new ArrayList<NeighborSolicitation>(); |
| NeighborSolicitation ns; |
| while ((ns = getNextNeighborSolicitation()) != null) { |
| // multicast NS for address resolution, IPv6 dst address in that 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) && ns.ipv6Hdr.dstIp.isMulticastAddress()) { |
| nsList.add(ns); |
| } |
| } |
| assertFalse(nsList.isEmpty()); |
| |
| if (expectNeighborLost) { |
| assertNotifyNeighborLost(targetIp, NudEventType.NUD_ORGANIC_FAILED_CRITICAL); |
| } else { |
| assertNeverNotifyNeighborLost(); |
| } |
| } |
| |
| @Test |
| @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION, enabled = true) |
| @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, enabled = false) |
| @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 /* expectNeighborLost */); |
| } |
| |
| @Test |
| @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION, enabled = false) |
| @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, enabled = false) |
| @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, |
| true /* expectNeighborLost */); |
| } |
| |
| @Test |
| @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION, enabled = false) |
| @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, enabled = true) |
| @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 /* expectNeighborLost */); |
| } |
| |
| @Test |
| @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION, enabled = false) |
| @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, enabled = false) |
| @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 */, |
| true /* expectNeighborLost */); |
| } |
| |
| @Test |
| @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION, enabled = false) |
| @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, enabled = false) |
| @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, |
| false /* expectNeighborLost */); |
| } |
| |
| @Test |
| @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION, enabled = false) |
| @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, enabled = false) |
| @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, |
| true /* expectNeighborLost */); |
| } |
| |
| @Test |
| @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION, enabled = false) |
| @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, enabled = false) |
| @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 */, |
| false /* expectNeighborLost */); |
| } |
| |
| @Test |
| @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION, enabled = false) |
| @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, enabled = false) |
| @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 /* expectNeighborLost */); |
| } |
| |
| private void runIpReachabilityMonitorIncompleteIpv6NeighborTest(final String dnsServer, |
| final Inet6Address targetIp) throws Exception { |
| prepareIpReachabilityMonitorAddressResolutionTest(dnsServer, targetIp); |
| |
| // Simulate the default router 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_INCOMPLETE_IPV6_DNS_SERVER_VERSION, enabled = false) |
| @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, enabled = true) |
| @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = false) |
| public void testIpReachabilityMonitor_ignoreIpv6DefaultRouter_everReachable() throws Exception { |
| runIpReachabilityMonitorIncompleteIpv6NeighborTest(IPV6_OFF_LINK_DNS_SERVER, |
| ROUTER_LINK_LOCAL /* targetIp */); |
| } |
| |
| @Test |
| @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION, enabled = true) |
| @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, enabled = false) |
| @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = false) |
| public void testIpReachabilityMonitor_ignoreIpv6Dns_everReachable() throws Exception { |
| runIpReachabilityMonitorIncompleteIpv6NeighborTest(IPV6_ON_LINK_DNS_SERVER, |
| ipv6Addr(IPV6_ON_LINK_DNS_SERVER) /* 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); |
| } |
| |
| @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 @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 |
| for (LinkAddress la : lp.getLinkAddresses()) { |
| final Inet6Address address = (Inet6Address) la.getAddress(); |
| if (address.isLinkLocalAddress()) continue; |
| // Remove IPv6 GUAs from interface. |
| mNetd.interfaceDelAddress(mIfaceName, address.getHostAddress(), la.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 { |
| final InOrder inOrder = inOrder(mCb); |
| 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 void prepareDhcp6PdTest() 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() |
| .withoutIPv4() |
| .build(); |
| startIpClientProvisioning(config); |
| |
| waitForRouterSolicitation(); |
| mPacketReader.sendResponse(ra); |
| } |
| |
| @Test |
| @Flag(name = IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION, enabled = true) |
| 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); |
| } |
| |
| @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 || lp.getRoutes().size() < 4) { |
| final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>(); |
| verifyWithTimeout(inOrder, mCb).onLinkPropertiesChange(argThat(x -> { |
| if (!x.isIpv6Provisioned()) return false; |
| if (x.getLinkAddresses().size() != 5) return false; |
| if (x.getRoutes().size() != 4) 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 |
| @Flag(name = IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION, enabled = true) |
| 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 |
| @Flag(name = IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION, enabled = true) |
| 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 |
| @Flag(name = IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION, enabled = true) |
| 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 |
| @Flag(name = IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION, enabled = true) |
| 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 */); |
| |
| clearInvocations(mCb); |
| |
| // 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 */)); |
| |
| // The IPv4 link address lifetime should be also updated after a success DHCP renew, check |
| // that we should never see provisioning failure. |
| verify(mCb, after(100).never()).onProvisioningFailure(any()); |
| |
| final ArgumentCaptor<DhcpResultsParcelable> dhcpResultsCaptor = |
| ArgumentCaptor.forClass(DhcpResultsParcelable.class); |
| verify(mCb, timeout(TEST_TIMEOUT_MS)).onNewDhcpResults(dhcpResultsCaptor.capture()); |
| final DhcpResultsParcelable lease = dhcpResultsCaptor.getValue(); |
| assertDhcpResultsParcelable(lease); |
| |
| // Check if the IPv4 address lifetime has updated along with a success DHCP renew. |
| verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(argThat(x -> { |
| for (LinkAddress la : x.getLinkAddresses()) { |
| if (la.isIpv4()) { |
| final long now = SystemClock.elapsedRealtime(); |
| final long when = now + 3600 * 1000; |
| return (la.getDeprecationTime() != LinkAddress.LIFETIME_UNKNOWN) |
| && (la.getExpirationTime() != LinkAddress.LIFETIME_UNKNOWN) |
| && (la.getDeprecationTime() < when + TEST_LIFETIME_TOLERANCE_MS) |
| && (la.getDeprecationTime() > when - TEST_LIFETIME_TOLERANCE_MS) |
| && (la.getExpirationTime() < when + TEST_LIFETIME_TOLERANCE_MS) |
| && (la.getExpirationTime() > when - TEST_LIFETIME_TOLERANCE_MS); |
| } |
| } |
| return false; |
| })); |
| } |
| |
| 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 */); |
| } |
| } |