diff options
Diffstat (limited to 'packages/NetworkStack/tests')
8 files changed, 1875 insertions, 0 deletions
diff --git a/packages/NetworkStack/tests/Android.bp b/packages/NetworkStack/tests/Android.bp new file mode 100644 index 000000000000..bd7ff2a75703 --- /dev/null +++ b/packages/NetworkStack/tests/Android.bp @@ -0,0 +1,35 @@ +// +// Copyright (C) 2018 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. +// + +android_test { + name: "NetworkStackTests", + srcs: ["src/**/*.java"], + static_libs: [ + "android-support-test", + "mockito-target-extended-minus-junit4", + "NetworkStackLib", + "testables", + ], + libs: [ + "android.test.runner", + "android.test.base", + ], + jni_libs: [ + // For mockito extended + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ] +}
\ No newline at end of file diff --git a/packages/NetworkStack/tests/AndroidManifest.xml b/packages/NetworkStack/tests/AndroidManifest.xml new file mode 100644 index 000000000000..8b8474f57e28 --- /dev/null +++ b/packages/NetworkStack/tests/AndroidManifest.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 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. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.server.networkstack.tests"> + <application android:debuggable="true"> + <uses-library android:name="android.test.runner" /> + </application> + <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.server.networkstack.tests" + android:label="Networking service tests"> + </instrumentation> +</manifest>
\ No newline at end of file diff --git a/packages/NetworkStack/tests/AndroidTest.xml b/packages/NetworkStack/tests/AndroidTest.xml new file mode 100644 index 000000000000..6b08b57731b7 --- /dev/null +++ b/packages/NetworkStack/tests/AndroidTest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 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. +--> +<configuration description="Runs Tests for NetworkStack"> + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="test-file-name" value="NetworkStackTests.apk" /> + </target_preparer> + + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="framework-base-presubmit" /> + <option name="test-tag" value="NetworkStackTests" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.server.networkstack.tests" /> + <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration>
\ No newline at end of file diff --git a/packages/NetworkStack/tests/src/android/net/dhcp/DhcpLeaseRepositoryTest.java b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpLeaseRepositoryTest.java new file mode 100644 index 000000000000..51d50d9eb13a --- /dev/null +++ b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpLeaseRepositoryTest.java @@ -0,0 +1,539 @@ +/* + * Copyright (C) 2018 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.dhcp; + +import static android.net.InetAddresses.parseNumericAddress; +import static android.net.dhcp.DhcpLease.HOSTNAME_NONE; +import static android.net.dhcp.DhcpLeaseRepository.CLIENTID_UNSPEC; +import static android.net.dhcp.DhcpLeaseRepository.INETADDR_UNSPEC; + +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.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.when; + +import static java.lang.String.format; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.IpPrefix; +import android.net.MacAddress; +import android.net.dhcp.DhcpServer.Clock; +import android.net.util.SharedLog; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.net.Inet4Address; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class DhcpLeaseRepositoryTest { + private static final Inet4Address INET4_ANY = (Inet4Address) Inet4Address.ANY; + private static final Inet4Address TEST_DEF_ROUTER = parseAddr4("192.168.42.247"); + private static final Inet4Address TEST_SERVER_ADDR = parseAddr4("192.168.42.241"); + private static final Inet4Address TEST_RESERVED_ADDR = parseAddr4("192.168.42.243"); + private static final MacAddress TEST_MAC_1 = MacAddress.fromBytes( + new byte[] { 5, 4, 3, 2, 1, 0 }); + private static final MacAddress TEST_MAC_2 = MacAddress.fromBytes( + new byte[] { 0, 1, 2, 3, 4, 5 }); + private static final MacAddress TEST_MAC_3 = MacAddress.fromBytes( + new byte[] { 0, 1, 2, 3, 4, 6 }); + private static final Inet4Address TEST_INETADDR_1 = parseAddr4("192.168.42.248"); + private static final Inet4Address TEST_INETADDR_2 = parseAddr4("192.168.42.249"); + private static final String TEST_HOSTNAME_1 = "hostname1"; + private static final String TEST_HOSTNAME_2 = "hostname2"; + private static final IpPrefix TEST_IP_PREFIX = new IpPrefix(TEST_SERVER_ADDR, 22); + private static final long TEST_TIME = 100L; + private static final int TEST_LEASE_TIME_MS = 3_600_000; + private static final Set<Inet4Address> TEST_EXCL_SET = + Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + TEST_SERVER_ADDR, TEST_DEF_ROUTER, TEST_RESERVED_ADDR))); + + @NonNull + private SharedLog mLog; + @NonNull @Mock + private Clock mClock; + @NonNull + private DhcpLeaseRepository mRepo; + + private static Inet4Address parseAddr4(String inet4Addr) { + return (Inet4Address) parseNumericAddress(inet4Addr); + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mLog = new SharedLog("DhcpLeaseRepositoryTest"); + when(mClock.elapsedRealtime()).thenReturn(TEST_TIME); + mRepo = new DhcpLeaseRepository( + TEST_IP_PREFIX, TEST_EXCL_SET, TEST_LEASE_TIME_MS, mLog, mClock); + } + + /** + * Request a number of addresses through offer/request. Useful to test address exhaustion. + * @param nAddr Number of addresses to request. + */ + private void requestAddresses(byte nAddr) throws Exception { + final HashSet<Inet4Address> addrs = new HashSet<>(); + byte[] hwAddrBytes = new byte[] { 8, 4, 3, 2, 1, 0 }; + for (byte i = 0; i < nAddr; i++) { + hwAddrBytes[5] = i; + MacAddress newMac = MacAddress.fromBytes(hwAddrBytes); + final String hostname = "host_" + i; + final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, newMac, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, hostname); + + assertNotNull(lease); + assertEquals(newMac, lease.getHwAddr()); + assertEquals(hostname, lease.getHostname()); + assertTrue(format("Duplicate address allocated: %s in %s", lease.getNetAddr(), addrs), + addrs.add(lease.getNetAddr())); + + requestLeaseSelecting(newMac, lease.getNetAddr(), hostname); + } + } + + @Test + public void testAddressExhaustion() throws Exception { + // Use a /28 to quickly run out of addresses + mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), TEST_EXCL_SET, TEST_LEASE_TIME_MS); + + // /28 should have 16 addresses, 14 w/o the first/last, 11 w/o excluded addresses + requestAddresses((byte) 11); + + try { + mRepo.getOffer(null, TEST_MAC_2, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + fail("Should be out of addresses"); + } catch (DhcpLeaseRepository.OutOfAddressesException e) { + // Expected + } + } + + @Test + public void testUpdateParams_LeaseCleanup() throws Exception { + // Inside /28: + final Inet4Address reqAddrIn28 = parseAddr4("192.168.42.242"); + final Inet4Address declinedAddrIn28 = parseAddr4("192.168.42.245"); + + // Inside /28, but not available there (first address of the range) + final Inet4Address declinedFirstAddrIn28 = parseAddr4("192.168.42.240"); + + final DhcpLease reqAddrIn28Lease = requestLeaseSelecting(TEST_MAC_1, reqAddrIn28); + mRepo.markLeaseDeclined(declinedAddrIn28); + mRepo.markLeaseDeclined(declinedFirstAddrIn28); + + // Inside /22, but outside /28: + final Inet4Address reqAddrIn22 = parseAddr4("192.168.42.3"); + final Inet4Address declinedAddrIn22 = parseAddr4("192.168.42.4"); + + final DhcpLease reqAddrIn22Lease = requestLeaseSelecting(TEST_MAC_3, reqAddrIn22); + mRepo.markLeaseDeclined(declinedAddrIn22); + + // Address that will be reserved in the updateParams call below + final Inet4Address reservedAddr = parseAddr4("192.168.42.244"); + final DhcpLease reservedAddrLease = requestLeaseSelecting(TEST_MAC_2, reservedAddr); + + // Update from /22 to /28 and add another reserved address + Set<Inet4Address> newReserved = new HashSet<>(TEST_EXCL_SET); + newReserved.add(reservedAddr); + mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), newReserved, TEST_LEASE_TIME_MS); + + assertHasLease(reqAddrIn28Lease); + assertDeclined(declinedAddrIn28); + + assertNotDeclined(declinedFirstAddrIn28); + + assertNoLease(reqAddrIn22Lease); + assertNotDeclined(declinedAddrIn22); + + assertNoLease(reservedAddrLease); + } + + @Test + public void testGetOffer_StableAddress() throws Exception { + for (final MacAddress macAddr : new MacAddress[] { TEST_MAC_1, TEST_MAC_2, TEST_MAC_3 }) { + final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, macAddr, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + + // Same lease is offered twice + final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, macAddr, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + assertEquals(lease, newLease); + } + } + + @Test + public void testUpdateParams_UsesNewPrefix() throws Exception { + final IpPrefix newPrefix = new IpPrefix(parseAddr4("192.168.123.0"), 24); + mRepo.updateParams(newPrefix, TEST_EXCL_SET, TEST_LEASE_TIME_MS); + + DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + assertTrue(newPrefix.contains(lease.getNetAddr())); + } + + @Test + public void testGetOffer_ExistingLease() throws Exception { + requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1, TEST_HOSTNAME_1); + + DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + assertEquals(TEST_INETADDR_1, offer.getNetAddr()); + assertEquals(TEST_HOSTNAME_1, offer.getHostname()); + } + + @Test + public void testGetOffer_ClientIdHasExistingLease() throws Exception { + final byte[] clientId = new byte[] { 1, 2 }; + mRepo.requestLease(clientId, TEST_MAC_1, INET4_ANY /* clientAddr */, + INET4_ANY /* relayAddr */, TEST_INETADDR_1 /* reqAddr */, false, TEST_HOSTNAME_1); + + // Different MAC, but same clientId + DhcpLease offer = mRepo.getOffer(clientId, TEST_MAC_2, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + assertEquals(TEST_INETADDR_1, offer.getNetAddr()); + assertEquals(TEST_HOSTNAME_1, offer.getHostname()); + } + + @Test + public void testGetOffer_DifferentClientId() throws Exception { + final byte[] clientId1 = new byte[] { 1, 2 }; + final byte[] clientId2 = new byte[] { 3, 4 }; + mRepo.requestLease(clientId1, TEST_MAC_1, INET4_ANY /* clientAddr */, + INET4_ANY /* relayAddr */, TEST_INETADDR_1 /* reqAddr */, false, TEST_HOSTNAME_1); + + // Same MAC, different client ID + DhcpLease offer = mRepo.getOffer(clientId2, TEST_MAC_1, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + // Obtains a different address + assertNotEquals(TEST_INETADDR_1, offer.getNetAddr()); + assertEquals(HOSTNAME_NONE, offer.getHostname()); + assertEquals(TEST_MAC_1, offer.getHwAddr()); + } + + @Test + public void testGetOffer_RequestedAddress() throws Exception { + DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* relayAddr */, + TEST_INETADDR_1 /* reqAddr */, TEST_HOSTNAME_1); + assertEquals(TEST_INETADDR_1, offer.getNetAddr()); + assertEquals(TEST_HOSTNAME_1, offer.getHostname()); + } + + @Test + public void testGetOffer_RequestedAddressInUse() throws Exception { + requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1); + DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_2, INET4_ANY /* relayAddr */, + TEST_INETADDR_1 /* reqAddr */, HOSTNAME_NONE); + assertNotEquals(TEST_INETADDR_1, offer.getNetAddr()); + } + + @Test + public void testGetOffer_RequestedAddressReserved() throws Exception { + DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* relayAddr */, + TEST_RESERVED_ADDR /* reqAddr */, HOSTNAME_NONE); + assertNotEquals(TEST_RESERVED_ADDR, offer.getNetAddr()); + } + + @Test + public void testGetOffer_RequestedAddressInvalid() throws Exception { + final Inet4Address invalidAddr = parseAddr4("192.168.42.0"); + DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* relayAddr */, + invalidAddr /* reqAddr */, HOSTNAME_NONE); + assertNotEquals(invalidAddr, offer.getNetAddr()); + } + + @Test + public void testGetOffer_RequestedAddressOutsideSubnet() throws Exception { + final Inet4Address invalidAddr = parseAddr4("192.168.254.2"); + DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* relayAddr */, + invalidAddr /* reqAddr */, HOSTNAME_NONE); + assertNotEquals(invalidAddr, offer.getNetAddr()); + } + + @Test(expected = DhcpLeaseRepository.InvalidSubnetException.class) + public void testGetOffer_RelayInInvalidSubnet() throws Exception { + mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, parseAddr4("192.168.254.2") /* relayAddr */, + INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + } + + @Test + public void testRequestLease_SelectingTwice() throws Exception { + final DhcpLease lease1 = requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1, + TEST_HOSTNAME_1); + + // Second request from same client for a different address + final DhcpLease lease2 = requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_2, + TEST_HOSTNAME_2); + + assertEquals(TEST_INETADDR_1, lease1.getNetAddr()); + assertEquals(TEST_HOSTNAME_1, lease1.getHostname()); + + assertEquals(TEST_INETADDR_2, lease2.getNetAddr()); + assertEquals(TEST_HOSTNAME_2, lease2.getHostname()); + + // First address freed when client requested a different one: another client can request it + final DhcpLease lease3 = requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1, HOSTNAME_NONE); + assertEquals(TEST_INETADDR_1, lease3.getNetAddr()); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_SelectingInvalid() throws Exception { + requestLeaseSelecting(TEST_MAC_1, parseAddr4("192.168.254.5")); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_SelectingInUse() throws Exception { + requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1); + requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_SelectingReserved() throws Exception { + requestLeaseSelecting(TEST_MAC_1, TEST_RESERVED_ADDR); + } + + @Test(expected = DhcpLeaseRepository.InvalidSubnetException.class) + public void testRequestLease_SelectingRelayInInvalidSubnet() throws Exception { + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* clientAddr */, + parseAddr4("192.168.128.1") /* relayAddr */, TEST_INETADDR_1 /* reqAddr */, + true /* sidSet */, HOSTNAME_NONE); + } + + @Test + public void testRequestLease_InitReboot() throws Exception { + // Request address once + requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1); + + final long newTime = TEST_TIME + 100; + when(mClock.elapsedRealtime()).thenReturn(newTime); + + // init-reboot (sidSet == false): verify configuration + final DhcpLease lease = requestLeaseInitReboot(TEST_MAC_1, TEST_INETADDR_1); + assertEquals(TEST_INETADDR_1, lease.getNetAddr()); + assertEquals(newTime + TEST_LEASE_TIME_MS, lease.getExpTime()); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_InitRebootWrongAddr() throws Exception { + // Request address once + requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1); + // init-reboot with different requested address + requestLeaseInitReboot(TEST_MAC_1, TEST_INETADDR_2); + } + + @Test + public void testRequestLease_InitRebootUnknownAddr() throws Exception { + // init-reboot with unknown requested address + final DhcpLease lease = requestLeaseInitReboot(TEST_MAC_1, TEST_INETADDR_2); + // RFC2131 says we should not reply to accommodate other servers, but since we are + // authoritative we allow creating the lease to avoid issues with lost lease DB (same as + // dnsmasq behavior) + assertEquals(TEST_INETADDR_2, lease.getNetAddr()); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_InitRebootWrongSubnet() throws Exception { + requestLeaseInitReboot(TEST_MAC_1, parseAddr4("192.168.254.2")); + } + + @Test + public void testRequestLease_Renewing() throws Exception { + requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1); + + final long newTime = TEST_TIME + 100; + when(mClock.elapsedRealtime()).thenReturn(newTime); + + final DhcpLease lease = requestLeaseRenewing(TEST_MAC_1, TEST_INETADDR_1); + + assertEquals(TEST_INETADDR_1, lease.getNetAddr()); + assertEquals(newTime + TEST_LEASE_TIME_MS, lease.getExpTime()); + } + + @Test + public void testRequestLease_RenewingUnknownAddr() throws Exception { + final long newTime = TEST_TIME + 100; + when(mClock.elapsedRealtime()).thenReturn(newTime); + final DhcpLease lease = requestLeaseRenewing(TEST_MAC_1, TEST_INETADDR_1); + // Allows renewing an unknown address if available + assertEquals(TEST_INETADDR_1, lease.getNetAddr()); + assertEquals(newTime + TEST_LEASE_TIME_MS, lease.getExpTime()); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_RenewingAddrInUse() throws Exception { + requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1); + requestLeaseRenewing(TEST_MAC_1, TEST_INETADDR_1); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_RenewingInvalidAddr() throws Exception { + requestLeaseRenewing(TEST_MAC_1, parseAddr4("192.168.254.2")); + } + + @Test + public void testReleaseLease() throws Exception { + final DhcpLease lease1 = requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1); + + assertHasLease(lease1); + assertTrue(mRepo.releaseLease(CLIENTID_UNSPEC, TEST_MAC_1, TEST_INETADDR_1)); + assertNoLease(lease1); + + final DhcpLease lease2 = requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1); + assertEquals(TEST_INETADDR_1, lease2.getNetAddr()); + } + + @Test + public void testReleaseLease_UnknownLease() { + assertFalse(mRepo.releaseLease(CLIENTID_UNSPEC, TEST_MAC_1, TEST_INETADDR_1)); + } + + @Test + public void testReleaseLease_StableOffer() throws Exception { + for (MacAddress mac : new MacAddress[] { TEST_MAC_1, TEST_MAC_2, TEST_MAC_3 }) { + final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, mac, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + + requestLeaseSelecting(mac, lease.getNetAddr()); + mRepo.releaseLease(CLIENTID_UNSPEC, mac, lease.getNetAddr()); + + // Same lease is offered after it was released + final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, mac, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + assertEquals(lease.getNetAddr(), newLease.getNetAddr()); + } + } + + @Test + public void testMarkLeaseDeclined() throws Exception { + final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + + mRepo.markLeaseDeclined(lease.getNetAddr()); + + // Same lease is not offered again + final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + assertNotEquals(lease.getNetAddr(), newLease.getNetAddr()); + } + + @Test + public void testMarkLeaseDeclined_UsedIfOutOfAddresses() throws Exception { + // Use a /28 to quickly run out of addresses + mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), TEST_EXCL_SET, TEST_LEASE_TIME_MS); + + mRepo.markLeaseDeclined(TEST_INETADDR_1); + mRepo.markLeaseDeclined(TEST_INETADDR_2); + + // /28 should have 16 addresses, 14 w/o the first/last, 11 w/o excluded addresses + requestAddresses((byte) 9); + + // Last 2 addresses: addresses marked declined should be used + final DhcpLease firstLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, TEST_HOSTNAME_1); + requestLeaseSelecting(TEST_MAC_1, firstLease.getNetAddr()); + + final DhcpLease secondLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_2, + INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, TEST_HOSTNAME_2); + requestLeaseSelecting(TEST_MAC_2, secondLease.getNetAddr()); + + // Now out of addresses + try { + mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_3, INET4_ANY /* relayAddr */, + INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + fail("Repository should be out of addresses and throw"); + } catch (DhcpLeaseRepository.OutOfAddressesException e) { /* expected */ } + + assertEquals(TEST_INETADDR_1, firstLease.getNetAddr()); + assertEquals(TEST_HOSTNAME_1, firstLease.getHostname()); + assertEquals(TEST_INETADDR_2, secondLease.getNetAddr()); + assertEquals(TEST_HOSTNAME_2, secondLease.getHostname()); + } + + private DhcpLease requestLease(@NonNull MacAddress macAddr, @NonNull Inet4Address clientAddr, + @Nullable Inet4Address reqAddr, @Nullable String hostname, boolean sidSet) + throws DhcpLeaseRepository.DhcpLeaseException { + return mRepo.requestLease(CLIENTID_UNSPEC, macAddr, clientAddr, INET4_ANY /* relayAddr */, + reqAddr, sidSet, hostname); + } + + /** + * Request a lease simulating a client in the SELECTING state. + */ + private DhcpLease requestLeaseSelecting(@NonNull MacAddress macAddr, + @NonNull Inet4Address reqAddr, @Nullable String hostname) + throws DhcpLeaseRepository.DhcpLeaseException { + return requestLease(macAddr, INET4_ANY /* clientAddr */, reqAddr, hostname, + true /* sidSet */); + } + + /** + * Request a lease simulating a client in the SELECTING state. + */ + private DhcpLease requestLeaseSelecting(@NonNull MacAddress macAddr, + @NonNull Inet4Address reqAddr) throws DhcpLeaseRepository.DhcpLeaseException { + return requestLeaseSelecting(macAddr, reqAddr, HOSTNAME_NONE); + } + + /** + * Request a lease simulating a client in the INIT-REBOOT state. + */ + private DhcpLease requestLeaseInitReboot(@NonNull MacAddress macAddr, + @NonNull Inet4Address reqAddr) throws DhcpLeaseRepository.DhcpLeaseException { + return requestLease(macAddr, INET4_ANY /* clientAddr */, reqAddr, HOSTNAME_NONE, + false /* sidSet */); + } + + /** + * Request a lease simulating a client in the RENEWING state. + */ + private DhcpLease requestLeaseRenewing(@NonNull MacAddress macAddr, + @NonNull Inet4Address clientAddr) throws DhcpLeaseRepository.DhcpLeaseException { + // Renewing: clientAddr filled in, no reqAddr + return requestLease(macAddr, clientAddr, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE, + true /* sidSet */); + } + + private void assertNoLease(DhcpLease lease) { + assertFalse("Leases contain " + lease, mRepo.getCommittedLeases().contains(lease)); + } + + private void assertHasLease(DhcpLease lease) { + assertTrue("Leases do not contain " + lease, mRepo.getCommittedLeases().contains(lease)); + } + + private void assertNotDeclined(Inet4Address addr) { + assertFalse("Address is declined: " + addr, mRepo.getDeclinedAddresses().contains(addr)); + } + + private void assertDeclined(Inet4Address addr) { + assertTrue("Address is not declined: " + addr, mRepo.getDeclinedAddresses().contains(addr)); + } +} diff --git a/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServerTest.java b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServerTest.java new file mode 100644 index 000000000000..d4c1e2e16731 --- /dev/null +++ b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServerTest.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2018 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.dhcp; + +import static android.net.InetAddresses.parseNumericAddress; +import static android.net.dhcp.DhcpPacket.DHCP_CLIENT; +import static android.net.dhcp.DhcpPacket.DHCP_HOST_NAME; +import static android.net.dhcp.DhcpPacket.ENCAP_BOOTP; +import static android.net.dhcp.DhcpPacket.INADDR_ANY; +import static android.net.dhcp.DhcpPacket.INADDR_BROADCAST; +import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.INetworkStackStatusCallback; +import android.net.LinkAddress; +import android.net.MacAddress; +import android.net.dhcp.DhcpLeaseRepository.InvalidAddressException; +import android.net.dhcp.DhcpLeaseRepository.OutOfAddressesException; +import android.net.dhcp.DhcpServer.Clock; +import android.net.dhcp.DhcpServer.Dependencies; +import android.net.util.SharedLog; +import android.os.HandlerThread; +import android.support.test.filters.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.testing.TestableLooper.RunWithLooper; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.net.Inet4Address; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +@RunWith(AndroidTestingRunner.class) +@SmallTest +@RunWithLooper +public class DhcpServerTest { + private static final String TEST_IFACE = "testiface"; + + private static final Inet4Address TEST_SERVER_ADDR = parseAddr("192.168.0.2"); + private static final LinkAddress TEST_SERVER_LINKADDR = new LinkAddress(TEST_SERVER_ADDR, 20); + private static final Set<Inet4Address> TEST_DEFAULT_ROUTERS = new HashSet<>( + Arrays.asList(parseAddr("192.168.0.123"), parseAddr("192.168.0.124"))); + private static final Set<Inet4Address> TEST_DNS_SERVERS = new HashSet<>( + Arrays.asList(parseAddr("192.168.0.126"), parseAddr("192.168.0.127"))); + private static final Set<Inet4Address> TEST_EXCLUDED_ADDRS = new HashSet<>( + Arrays.asList(parseAddr("192.168.0.200"), parseAddr("192.168.0.201"))); + private static final long TEST_LEASE_TIME_SECS = 3600L; + private static final int TEST_MTU = 1500; + private static final String TEST_HOSTNAME = "testhostname"; + + private static final int TEST_TRANSACTION_ID = 123; + private static final byte[] TEST_CLIENT_MAC_BYTES = new byte [] { 1, 2, 3, 4, 5, 6 }; + private static final MacAddress TEST_CLIENT_MAC = MacAddress.fromBytes(TEST_CLIENT_MAC_BYTES); + private static final Inet4Address TEST_CLIENT_ADDR = parseAddr("192.168.0.42"); + + private static final long TEST_CLOCK_TIME = 1234L; + private static final int TEST_LEASE_EXPTIME_SECS = 3600; + private static final DhcpLease TEST_LEASE = new DhcpLease(null, TEST_CLIENT_MAC, + TEST_CLIENT_ADDR, TEST_LEASE_EXPTIME_SECS * 1000L + TEST_CLOCK_TIME, + null /* hostname */); + private static final DhcpLease TEST_LEASE_WITH_HOSTNAME = new DhcpLease(null, TEST_CLIENT_MAC, + TEST_CLIENT_ADDR, TEST_LEASE_EXPTIME_SECS * 1000L + TEST_CLOCK_TIME, TEST_HOSTNAME); + + @NonNull @Mock + private Dependencies mDeps; + @NonNull @Mock + private DhcpLeaseRepository mRepository; + @NonNull @Mock + private Clock mClock; + @NonNull @Mock + private DhcpPacketListener mPacketListener; + + @NonNull @Captor + private ArgumentCaptor<ByteBuffer> mSentPacketCaptor; + @NonNull @Captor + private ArgumentCaptor<Inet4Address> mResponseDstAddrCaptor; + + @NonNull + private HandlerThread mHandlerThread; + @NonNull + private TestableLooper mLooper; + @NonNull + private DhcpServer mServer; + + @Nullable + private String mPrevShareClassloaderProp; + + private final INetworkStackStatusCallback mAssertSuccessCallback = + new INetworkStackStatusCallback.Stub() { + @Override + public void onStatusAvailable(int statusCode) { + assertEquals(STATUS_SUCCESS, statusCode); + } + }; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + when(mDeps.makeLeaseRepository(any(), any(), any())).thenReturn(mRepository); + when(mDeps.makeClock()).thenReturn(mClock); + when(mDeps.makePacketListener()).thenReturn(mPacketListener); + doNothing().when(mDeps) + .sendPacket(any(), mSentPacketCaptor.capture(), mResponseDstAddrCaptor.capture()); + when(mClock.elapsedRealtime()).thenReturn(TEST_CLOCK_TIME); + + final DhcpServingParams servingParams = new DhcpServingParams.Builder() + .setDefaultRouters(TEST_DEFAULT_ROUTERS) + .setDhcpLeaseTimeSecs(TEST_LEASE_TIME_SECS) + .setDnsServers(TEST_DNS_SERVERS) + .setServerAddr(TEST_SERVER_LINKADDR) + .setLinkMtu(TEST_MTU) + .setExcludedAddrs(TEST_EXCLUDED_ADDRS) + .build(); + + mLooper = TestableLooper.get(this); + mHandlerThread = spy(new HandlerThread("TestDhcpServer")); + when(mHandlerThread.getLooper()).thenReturn(mLooper.getLooper()); + mServer = new DhcpServer(mHandlerThread, TEST_IFACE, servingParams, + new SharedLog(DhcpServerTest.class.getSimpleName()), mDeps); + + mServer.start(mAssertSuccessCallback); + mLooper.processAllMessages(); + } + + @After + public void tearDown() throws Exception { + mServer.stop(mAssertSuccessCallback); + mLooper.processMessages(1); + verify(mPacketListener, times(1)).stop(); + verify(mHandlerThread, times(1)).quitSafely(); + } + + @Test + public void testStart() throws Exception { + verify(mPacketListener, times(1)).start(); + } + + @Test + public void testDiscover() throws Exception { + // TODO: refactor packet construction to eliminate unnecessary/confusing/duplicate fields + when(mRepository.getOffer(isNull() /* clientId */, eq(TEST_CLIENT_MAC), + eq(INADDR_ANY) /* relayAddr */, isNull() /* reqAddr */, isNull() /* hostname */)) + .thenReturn(TEST_LEASE); + + final DhcpDiscoverPacket discover = new DhcpDiscoverPacket(TEST_TRANSACTION_ID, + (short) 0 /* secs */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES, + false /* broadcast */, INADDR_ANY /* srcIp */); + mServer.processPacket(discover, DHCP_CLIENT); + + assertResponseSentTo(TEST_CLIENT_ADDR); + final DhcpOfferPacket packet = assertOffer(getPacket()); + assertMatchesTestLease(packet); + } + + @Test + public void testDiscover_OutOfAddresses() throws Exception { + when(mRepository.getOffer(isNull() /* clientId */, eq(TEST_CLIENT_MAC), + eq(INADDR_ANY) /* relayAddr */, isNull() /* reqAddr */, isNull() /* hostname */)) + .thenThrow(new OutOfAddressesException("Test exception")); + + final DhcpDiscoverPacket discover = new DhcpDiscoverPacket(TEST_TRANSACTION_ID, + (short) 0 /* secs */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES, + false /* broadcast */, INADDR_ANY /* srcIp */); + mServer.processPacket(discover, DHCP_CLIENT); + + assertResponseSentTo(INADDR_BROADCAST); + final DhcpNakPacket packet = assertNak(getPacket()); + assertMatchesClient(packet); + } + + private DhcpRequestPacket makeRequestSelectingPacket() { + final DhcpRequestPacket request = new DhcpRequestPacket(TEST_TRANSACTION_ID, + (short) 0 /* secs */, INADDR_ANY /* clientIp */, INADDR_ANY /* relayIp */, + TEST_CLIENT_MAC_BYTES, false /* broadcast */); + request.mServerIdentifier = TEST_SERVER_ADDR; + request.mRequestedIp = TEST_CLIENT_ADDR; + return request; + } + + @Test + public void testRequest_Selecting_Ack() throws Exception { + when(mRepository.requestLease(isNull() /* clientId */, eq(TEST_CLIENT_MAC), + eq(INADDR_ANY) /* clientAddr */, eq(INADDR_ANY) /* relayAddr */, + eq(TEST_CLIENT_ADDR) /* reqAddr */, eq(true) /* sidSet */, eq(TEST_HOSTNAME))) + .thenReturn(TEST_LEASE_WITH_HOSTNAME); + + final DhcpRequestPacket request = makeRequestSelectingPacket(); + request.mHostName = TEST_HOSTNAME; + request.mRequestedParams = new byte[] { DHCP_HOST_NAME }; + mServer.processPacket(request, DHCP_CLIENT); + + assertResponseSentTo(TEST_CLIENT_ADDR); + final DhcpAckPacket packet = assertAck(getPacket()); + assertMatchesTestLease(packet, TEST_HOSTNAME); + } + + @Test + public void testRequest_Selecting_Nak() throws Exception { + when(mRepository.requestLease(isNull(), eq(TEST_CLIENT_MAC), + eq(INADDR_ANY) /* clientAddr */, eq(INADDR_ANY) /* relayAddr */, + eq(TEST_CLIENT_ADDR) /* reqAddr */, eq(true) /* sidSet */, isNull() /* hostname */)) + .thenThrow(new InvalidAddressException("Test error")); + + final DhcpRequestPacket request = makeRequestSelectingPacket(); + mServer.processPacket(request, DHCP_CLIENT); + + assertResponseSentTo(INADDR_BROADCAST); + final DhcpNakPacket packet = assertNak(getPacket()); + assertMatchesClient(packet); + } + + @Test + public void testRequest_Selecting_WrongClientPort() throws Exception { + final DhcpRequestPacket request = makeRequestSelectingPacket(); + mServer.processPacket(request, 50000); + + verify(mRepository, never()) + .requestLease(any(), any(), any(), any(), any(), anyBoolean(), any()); + verify(mDeps, never()).sendPacket(any(), any(), any()); + } + + @Test + public void testRelease() throws Exception { + final DhcpReleasePacket release = new DhcpReleasePacket(TEST_TRANSACTION_ID, + TEST_SERVER_ADDR, TEST_CLIENT_ADDR, + INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES); + mServer.processPacket(release, DHCP_CLIENT); + + verify(mRepository, times(1)) + .releaseLease(isNull(), eq(TEST_CLIENT_MAC), eq(TEST_CLIENT_ADDR)); + } + + /* TODO: add more tests once packet construction is refactored, including: + * - usage of giaddr + * - usage of broadcast bit + * - other request states (init-reboot/renewing/rebinding) + */ + + private void assertMatchesTestLease(@NonNull DhcpPacket packet, @Nullable String hostname) { + assertMatchesClient(packet); + assertFalse(packet.hasExplicitClientId()); + assertEquals(TEST_SERVER_ADDR, packet.mServerIdentifier); + assertEquals(TEST_CLIENT_ADDR, packet.mYourIp); + assertNotNull(packet.mLeaseTime); + assertEquals(TEST_LEASE_EXPTIME_SECS, (int) packet.mLeaseTime); + assertEquals(hostname, packet.mHostName); + } + + private void assertMatchesTestLease(@NonNull DhcpPacket packet) { + assertMatchesTestLease(packet, null); + } + + private void assertMatchesClient(@NonNull DhcpPacket packet) { + assertEquals(TEST_TRANSACTION_ID, packet.mTransId); + assertEquals(TEST_CLIENT_MAC, MacAddress.fromBytes(packet.mClientMac)); + } + + private void assertResponseSentTo(@NonNull Inet4Address addr) { + assertEquals(addr, mResponseDstAddrCaptor.getValue()); + } + + private static DhcpNakPacket assertNak(@Nullable DhcpPacket packet) { + assertTrue(packet instanceof DhcpNakPacket); + return (DhcpNakPacket) packet; + } + + private static DhcpAckPacket assertAck(@Nullable DhcpPacket packet) { + assertTrue(packet instanceof DhcpAckPacket); + return (DhcpAckPacket) packet; + } + + private static DhcpOfferPacket assertOffer(@Nullable DhcpPacket packet) { + assertTrue(packet instanceof DhcpOfferPacket); + return (DhcpOfferPacket) packet; + } + + private DhcpPacket getPacket() throws Exception { + verify(mDeps, times(1)).sendPacket(any(), any(), any()); + return DhcpPacket.decodeFullPacket(mSentPacketCaptor.getValue(), ENCAP_BOOTP); + } + + private static Inet4Address parseAddr(@Nullable String inet4Addr) { + return (Inet4Address) parseNumericAddress(inet4Addr); + } +} diff --git a/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServingParamsTest.java b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServingParamsTest.java new file mode 100644 index 000000000000..3ca0564f24d6 --- /dev/null +++ b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServingParamsTest.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2018 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.dhcp; + +import static android.net.InetAddresses.parseNumericAddress; +import static android.net.NetworkUtils.inet4AddressToIntHTH; +import static android.net.dhcp.DhcpServingParams.MTU_UNSET; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.LinkAddress; +import android.net.NetworkUtils; +import android.net.dhcp.DhcpServingParams.InvalidParameterException; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.lang.reflect.Modifier; +import java.net.Inet4Address; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class DhcpServingParamsTest { + @NonNull + private DhcpServingParams.Builder mBuilder; + + private static final Set<Inet4Address> TEST_DEFAULT_ROUTERS = new HashSet<>( + Arrays.asList(parseAddr("192.168.0.123"), parseAddr("192.168.0.124"))); + private static final long TEST_LEASE_TIME_SECS = 3600L; + private static final Set<Inet4Address> TEST_DNS_SERVERS = new HashSet<>( + Arrays.asList(parseAddr("192.168.0.126"), parseAddr("192.168.0.127"))); + private static final Inet4Address TEST_SERVER_ADDR = parseAddr("192.168.0.2"); + private static final LinkAddress TEST_LINKADDR = new LinkAddress(TEST_SERVER_ADDR, 20); + private static final int TEST_MTU = 1500; + private static final Set<Inet4Address> TEST_EXCLUDED_ADDRS = new HashSet<>( + Arrays.asList(parseAddr("192.168.0.200"), parseAddr("192.168.0.201"))); + private static final boolean TEST_METERED = true; + + @Before + public void setUp() { + mBuilder = new DhcpServingParams.Builder() + .setDefaultRouters(TEST_DEFAULT_ROUTERS) + .setDhcpLeaseTimeSecs(TEST_LEASE_TIME_SECS) + .setDnsServers(TEST_DNS_SERVERS) + .setServerAddr(TEST_LINKADDR) + .setLinkMtu(TEST_MTU) + .setExcludedAddrs(TEST_EXCLUDED_ADDRS) + .setMetered(TEST_METERED); + } + + @Test + public void testBuild_Immutable() throws InvalidParameterException { + final Set<Inet4Address> routers = new HashSet<>(TEST_DEFAULT_ROUTERS); + final Set<Inet4Address> dnsServers = new HashSet<>(TEST_DNS_SERVERS); + final Set<Inet4Address> excludedAddrs = new HashSet<>(TEST_EXCLUDED_ADDRS); + + final DhcpServingParams params = mBuilder + .setDefaultRouters(routers) + .setDnsServers(dnsServers) + .setExcludedAddrs(excludedAddrs) + .build(); + + // Modifications to source objects should not affect builder or final parameters + final Inet4Address addedAddr = parseAddr("192.168.0.223"); + routers.add(addedAddr); + dnsServers.add(addedAddr); + excludedAddrs.add(addedAddr); + + assertEquals(TEST_DEFAULT_ROUTERS, params.defaultRouters); + assertEquals(TEST_LEASE_TIME_SECS, params.dhcpLeaseTimeSecs); + assertEquals(TEST_DNS_SERVERS, params.dnsServers); + assertEquals(TEST_LINKADDR, params.serverAddr); + assertEquals(TEST_MTU, params.linkMtu); + assertEquals(TEST_METERED, params.metered); + + assertContains(params.excludedAddrs, TEST_EXCLUDED_ADDRS); + assertContains(params.excludedAddrs, TEST_DEFAULT_ROUTERS); + assertContains(params.excludedAddrs, TEST_DNS_SERVERS); + assertContains(params.excludedAddrs, TEST_SERVER_ADDR); + + assertFalse("excludedAddrs should not contain " + addedAddr, + params.excludedAddrs.contains(addedAddr)); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_NegativeLeaseTime() throws InvalidParameterException { + mBuilder.setDhcpLeaseTimeSecs(-1).build(); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_LeaseTimeTooLarge() throws InvalidParameterException { + // Set lease time larger than max value for uint32 + mBuilder.setDhcpLeaseTimeSecs(1L << 32).build(); + } + + @Test + public void testBuild_InfiniteLeaseTime() throws InvalidParameterException { + final long infiniteLeaseTime = 0xffffffffL; + final DhcpServingParams params = mBuilder + .setDhcpLeaseTimeSecs(infiniteLeaseTime).build(); + assertEquals(infiniteLeaseTime, params.dhcpLeaseTimeSecs); + assertTrue(params.dhcpLeaseTimeSecs > 0L); + } + + @Test + public void testBuild_UnsetMtu() throws InvalidParameterException { + final DhcpServingParams params = mBuilder.setLinkMtu(MTU_UNSET).build(); + assertEquals(MTU_UNSET, params.linkMtu); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_MtuTooSmall() throws InvalidParameterException { + mBuilder.setLinkMtu(20).build(); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_MtuTooLarge() throws InvalidParameterException { + mBuilder.setLinkMtu(65_536).build(); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_IPv6Addr() throws InvalidParameterException { + mBuilder.setServerAddr(new LinkAddress(parseNumericAddress("fe80::1111"), 120)).build(); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_PrefixTooLarge() throws InvalidParameterException { + mBuilder.setServerAddr(new LinkAddress(TEST_SERVER_ADDR, 15)).build(); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_PrefixTooSmall() throws InvalidParameterException { + mBuilder.setDefaultRouters(parseAddr("192.168.0.254")) + .setServerAddr(new LinkAddress(TEST_SERVER_ADDR, 31)) + .build(); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_RouterNotInPrefix() throws InvalidParameterException { + mBuilder.setDefaultRouters(parseAddr("192.168.254.254")).build(); + } + + @Test + public void testFromParcelableObject() throws InvalidParameterException { + final DhcpServingParams params = mBuilder.build(); + final DhcpServingParamsParcel parcel = new DhcpServingParamsParcel(); + parcel.defaultRouters = toIntArray(TEST_DEFAULT_ROUTERS); + parcel.dhcpLeaseTimeSecs = TEST_LEASE_TIME_SECS; + parcel.dnsServers = toIntArray(TEST_DNS_SERVERS); + parcel.serverAddr = inet4AddressToIntHTH(TEST_SERVER_ADDR); + parcel.serverAddrPrefixLength = TEST_LINKADDR.getPrefixLength(); + parcel.linkMtu = TEST_MTU; + parcel.excludedAddrs = toIntArray(TEST_EXCLUDED_ADDRS); + parcel.metered = TEST_METERED; + final DhcpServingParams parceled = DhcpServingParams.fromParcelableObject(parcel); + + assertEquals(params.defaultRouters, parceled.defaultRouters); + assertEquals(params.dhcpLeaseTimeSecs, parceled.dhcpLeaseTimeSecs); + assertEquals(params.dnsServers, parceled.dnsServers); + assertEquals(params.serverAddr, parceled.serverAddr); + assertEquals(params.linkMtu, parceled.linkMtu); + assertEquals(params.excludedAddrs, parceled.excludedAddrs); + assertEquals(params.metered, parceled.metered); + + // Ensure that we do not miss any field if added in the future + final long numFields = Arrays.stream(DhcpServingParams.class.getDeclaredFields()) + .filter(f -> !Modifier.isStatic(f.getModifiers())) + .count(); + assertEquals(7, numFields); + } + + @Test(expected = InvalidParameterException.class) + public void testFromParcelableObject_NullArgument() throws InvalidParameterException { + DhcpServingParams.fromParcelableObject(null); + } + + private static int[] toIntArray(Collection<Inet4Address> addrs) { + return addrs.stream().mapToInt(NetworkUtils::inet4AddressToIntHTH).toArray(); + } + + private static <T> void assertContains(@NonNull Set<T> set, @NonNull Set<T> subset) { + for (final T elem : subset) { + assertContains(set, elem); + } + } + + private static <T> void assertContains(@NonNull Set<T> set, @Nullable T elem) { + assertTrue("Set does not contain " + elem, set.contains(elem)); + } + + @NonNull + private static Inet4Address parseAddr(@NonNull String inet4Addr) { + return (Inet4Address) parseNumericAddress(inet4Addr); + } +} diff --git a/packages/NetworkStack/tests/src/com/android/server/connectivity/NetworkMonitorTest.java b/packages/NetworkStack/tests/src/com/android/server/connectivity/NetworkMonitorTest.java new file mode 100644 index 000000000000..d31fa7732e66 --- /dev/null +++ b/packages/NetworkStack/tests/src/com/android/server/connectivity/NetworkMonitorTest.java @@ -0,0 +1,604 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import static android.net.ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN; +import static android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL; +import static android.net.INetworkMonitor.NETWORK_TEST_RESULT_INVALID; +import static android.net.INetworkMonitor.NETWORK_TEST_RESULT_VALID; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +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.content.Context; +import android.content.Intent; +import android.net.CaptivePortal; +import android.net.ConnectivityManager; +import android.net.INetworkMonitorCallbacks; +import android.net.InetAddresses; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.NetworkRequest; +import android.net.captiveportal.CaptivePortalProbeResult; +import android.net.metrics.IpConnectivityLog; +import android.net.util.SharedLog; +import android.net.wifi.WifiManager; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.SystemClock; +import android.os.UserHandle; +import android.provider.Settings; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.telephony.TelephonyManager; +import android.util.ArrayMap; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.URL; +import java.util.Random; + +import javax.net.ssl.SSLHandshakeException; + + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkMonitorTest { + private static final String LOCATION_HEADER = "location"; + + private @Mock Context mContext; + private @Mock IpConnectivityLog mLogger; + private @Mock SharedLog mValidationLogger; + private @Mock NetworkInfo mNetworkInfo; + private @Mock ConnectivityManager mCm; + private @Mock TelephonyManager mTelephony; + private @Mock WifiManager mWifi; + private @Mock HttpURLConnection mHttpConnection; + private @Mock HttpURLConnection mHttpsConnection; + private @Mock HttpURLConnection mFallbackConnection; + private @Mock HttpURLConnection mOtherFallbackConnection; + private @Mock Random mRandom; + private @Mock NetworkMonitor.Dependencies mDependencies; + private @Mock INetworkMonitorCallbacks mCallbacks; + private @Spy Network mNetwork = new Network(TEST_NETID); + private NetworkRequest mRequest; + + private static final int TEST_NETID = 4242; + + private static final String TEST_HTTP_URL = "http://www.google.com/gen_204"; + private static final String TEST_HTTPS_URL = "https://www.google.com/gen_204"; + private static final String TEST_FALLBACK_URL = "http://fallback.google.com/gen_204"; + private static final String TEST_OTHER_FALLBACK_URL = "http://otherfallback.google.com/gen_204"; + + private static final int DATA_STALL_EVALUATION_TYPE_DNS = 1; + private static final int RETURN_CODE_DNS_SUCCESS = 0; + private static final int RETURN_CODE_DNS_TIMEOUT = 255; + + private static final int HANDLER_TIMEOUT_MS = 1000; + + private static final LinkProperties TEST_LINKPROPERTIES = new LinkProperties(); + + private static final NetworkCapabilities METERED_CAPABILITIES = new NetworkCapabilities() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_INTERNET); + + private static final NetworkCapabilities NOT_METERED_CAPABILITIES = new NetworkCapabilities() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED); + + private static final NetworkCapabilities NO_INTERNET_CAPABILITIES = new NetworkCapabilities() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR); + + @Before + public void setUp() throws IOException { + MockitoAnnotations.initMocks(this); + when(mDependencies.getPrivateDnsBypassNetwork(any())).thenReturn(mNetwork); + when(mDependencies.getRandom()).thenReturn(mRandom); + when(mDependencies.getSetting(any(), eq(Settings.Global.CAPTIVE_PORTAL_MODE), anyInt())) + .thenReturn(Settings.Global.CAPTIVE_PORTAL_MODE_PROMPT); + when(mDependencies.getSetting(any(), eq(Settings.Global.CAPTIVE_PORTAL_USE_HTTPS), + anyInt())).thenReturn(1); + when(mDependencies.getCaptivePortalServerHttpUrl(any())).thenReturn(TEST_HTTP_URL); + when(mDependencies.getSetting(any(), eq(Settings.Global.CAPTIVE_PORTAL_HTTPS_URL), + anyString())).thenReturn(TEST_HTTPS_URL); + doReturn(mNetwork).when(mNetwork).getPrivateDnsBypassingCopy(); + + when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(mCm); + when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephony); + when(mContext.getSystemService(Context.WIFI_SERVICE)).thenReturn(mWifi); + + when(mNetworkInfo.getType()).thenReturn(ConnectivityManager.TYPE_WIFI); + setFallbackUrl(TEST_FALLBACK_URL); + setOtherFallbackUrls(TEST_OTHER_FALLBACK_URL); + setFallbackSpecs(null); // Test with no fallback spec by default + when(mRandom.nextInt()).thenReturn(0); + + doAnswer((invocation) -> { + URL url = invocation.getArgument(0); + switch(url.toString()) { + case TEST_HTTP_URL: + return mHttpConnection; + case TEST_HTTPS_URL: + return mHttpsConnection; + case TEST_FALLBACK_URL: + return mFallbackConnection; + case TEST_OTHER_FALLBACK_URL: + return mOtherFallbackConnection; + default: + fail("URL not mocked: " + url.toString()); + return null; + } + }).when(mNetwork).openConnection(any()); + when(mHttpConnection.getRequestProperties()).thenReturn(new ArrayMap<>()); + when(mHttpsConnection.getRequestProperties()).thenReturn(new ArrayMap<>()); + doReturn(new InetAddress[] { + InetAddresses.parseNumericAddress("192.168.0.0") + }).when(mNetwork).getAllByName(any()); + + mRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_INTERNET) + .addCapability(NET_CAPABILITY_NOT_RESTRICTED) + .build(); + // Default values. Individual tests can override these. + when(mCm.getLinkProperties(any())).thenReturn(TEST_LINKPROPERTIES); + when(mCm.getNetworkCapabilities(any())).thenReturn(METERED_CAPABILITIES); + + setMinDataStallEvaluateInterval(500); + setDataStallEvaluationType(1 << DATA_STALL_EVALUATION_TYPE_DNS); + setValidDataStallDnsTimeThreshold(500); + setConsecutiveDnsTimeoutThreshold(5); + } + + private class WrappedNetworkMonitor extends NetworkMonitor { + private long mProbeTime = 0; + + WrappedNetworkMonitor(Context context, Network network, NetworkRequest defaultRequest, + IpConnectivityLog logger, Dependencies deps) { + super(context, mCallbacks, network, defaultRequest, logger, + new SharedLog("test_nm"), deps); + } + + @Override + protected long getLastProbeTime() { + return mProbeTime; + } + + protected void setLastProbeTime(long time) { + mProbeTime = time; + } + } + + private WrappedNetworkMonitor makeMeteredWrappedNetworkMonitor() { + final WrappedNetworkMonitor nm = new WrappedNetworkMonitor( + mContext, mNetwork, mRequest, mLogger, mDependencies); + when(mCm.getNetworkCapabilities(any())).thenReturn(METERED_CAPABILITIES); + nm.start(); + waitForIdle(nm.getHandler()); + return nm; + } + + private WrappedNetworkMonitor makeNotMeteredWrappedNetworkMonitor() { + final WrappedNetworkMonitor nm = new WrappedNetworkMonitor( + mContext, mNetwork, mRequest, mLogger, mDependencies); + when(mCm.getNetworkCapabilities(any())).thenReturn(NOT_METERED_CAPABILITIES); + nm.start(); + waitForIdle(nm.getHandler()); + return nm; + } + + private NetworkMonitor makeMonitor() { + final NetworkMonitor nm = new NetworkMonitor( + mContext, mCallbacks, mNetwork, mRequest, mLogger, mValidationLogger, + mDependencies); + nm.start(); + waitForIdle(nm.getHandler()); + return nm; + } + + private void waitForIdle(Handler handler) { + final ConditionVariable cv = new ConditionVariable(false); + handler.post(cv::open); + if (!cv.block(HANDLER_TIMEOUT_MS)) { + fail("Timed out waiting for handler"); + } + } + + @Test + public void testIsCaptivePortal_HttpProbeIsPortal() throws IOException { + setSslException(mHttpsConnection); + setPortal302(mHttpConnection); + + assertPortal(makeMonitor().isCaptivePortal()); + } + + @Test + public void testIsCaptivePortal_HttpsProbeIsNotPortal() throws IOException { + setStatus(mHttpsConnection, 204); + setStatus(mHttpConnection, 500); + + assertNotPortal(makeMonitor().isCaptivePortal()); + } + + @Test + public void testIsCaptivePortal_HttpsProbeFailedHttpSuccessNotUsed() throws IOException { + setSslException(mHttpsConnection); + // Even if HTTP returns a 204, do not use the result unless HTTPS succeeded + setStatus(mHttpConnection, 204); + setStatus(mFallbackConnection, 500); + + assertFailed(makeMonitor().isCaptivePortal()); + } + + @Test + public void testIsCaptivePortal_FallbackProbeIsPortal() throws IOException { + setSslException(mHttpsConnection); + setStatus(mHttpConnection, 500); + setPortal302(mFallbackConnection); + + assertPortal(makeMonitor().isCaptivePortal()); + } + + @Test + public void testIsCaptivePortal_FallbackProbeIsNotPortal() throws IOException { + setSslException(mHttpsConnection); + setStatus(mHttpConnection, 500); + setStatus(mFallbackConnection, 204); + + // Fallback probe did not see portal, HTTPS failed -> inconclusive + assertFailed(makeMonitor().isCaptivePortal()); + } + + @Test + public void testIsCaptivePortal_OtherFallbackProbeIsPortal() throws IOException { + // Set all fallback probes but one to invalid URLs to verify they are being skipped + setFallbackUrl(TEST_FALLBACK_URL); + setOtherFallbackUrls(TEST_FALLBACK_URL + "," + TEST_OTHER_FALLBACK_URL); + + setSslException(mHttpsConnection); + setStatus(mHttpConnection, 500); + setStatus(mFallbackConnection, 500); + setPortal302(mOtherFallbackConnection); + + // TEST_OTHER_FALLBACK_URL is third + when(mRandom.nextInt()).thenReturn(2); + + final NetworkMonitor monitor = makeMonitor(); + + // First check always uses the first fallback URL: inconclusive + assertFailed(monitor.isCaptivePortal()); + verify(mFallbackConnection, times(1)).getResponseCode(); + verify(mOtherFallbackConnection, never()).getResponseCode(); + + // Second check uses the URL chosen by Random + assertPortal(monitor.isCaptivePortal()); + verify(mOtherFallbackConnection, times(1)).getResponseCode(); + } + + @Test + public void testIsCaptivePortal_AllProbesFailed() throws IOException { + setSslException(mHttpsConnection); + setStatus(mHttpConnection, 500); + setStatus(mFallbackConnection, 404); + + assertFailed(makeMonitor().isCaptivePortal()); + verify(mFallbackConnection, times(1)).getResponseCode(); + verify(mOtherFallbackConnection, never()).getResponseCode(); + } + + @Test + public void testIsCaptivePortal_InvalidUrlSkipped() throws IOException { + setFallbackUrl("invalid"); + setOtherFallbackUrls("otherinvalid," + TEST_OTHER_FALLBACK_URL + ",yetanotherinvalid"); + + setSslException(mHttpsConnection); + setStatus(mHttpConnection, 500); + setPortal302(mOtherFallbackConnection); + + assertPortal(makeMonitor().isCaptivePortal()); + verify(mOtherFallbackConnection, times(1)).getResponseCode(); + verify(mFallbackConnection, never()).getResponseCode(); + } + + private void setupFallbackSpec() throws IOException { + setFallbackSpecs("http://example.com@@/@@204@@/@@" + + "@@,@@" + + TEST_OTHER_FALLBACK_URL + "@@/@@30[12]@@/@@https://(www\\.)?google.com/?.*"); + + setSslException(mHttpsConnection); + setStatus(mHttpConnection, 500); + + // Use the 2nd fallback spec + when(mRandom.nextInt()).thenReturn(1); + } + + @Test + public void testIsCaptivePortal_FallbackSpecIsNotPortal() throws IOException { + setupFallbackSpec(); + set302(mOtherFallbackConnection, "https://www.google.com/test?q=3"); + + // HTTPS failed, fallback spec did not see a portal -> inconclusive + assertFailed(makeMonitor().isCaptivePortal()); + verify(mOtherFallbackConnection, times(1)).getResponseCode(); + verify(mFallbackConnection, never()).getResponseCode(); + } + + @Test + public void testIsCaptivePortal_FallbackSpecIsPortal() throws IOException { + setupFallbackSpec(); + set302(mOtherFallbackConnection, "http://login.portal.example.com"); + + assertPortal(makeMonitor().isCaptivePortal()); + } + + @Test + public void testIsCaptivePortal_IgnorePortals() throws IOException { + setCaptivePortalMode(Settings.Global.CAPTIVE_PORTAL_MODE_IGNORE); + setSslException(mHttpsConnection); + setPortal302(mHttpConnection); + + assertNotPortal(makeMonitor().isCaptivePortal()); + } + + @Test + public void testIsDataStall_EvaluationDisabled() { + setDataStallEvaluationType(0); + WrappedNetworkMonitor wrappedMonitor = makeMeteredWrappedNetworkMonitor(); + wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 100); + assertFalse(wrappedMonitor.isDataStall()); + } + + @Test + public void testIsDataStall_EvaluationDnsOnNotMeteredNetwork() { + WrappedNetworkMonitor wrappedMonitor = makeNotMeteredWrappedNetworkMonitor(); + wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 100); + makeDnsTimeoutEvent(wrappedMonitor, 5); + assertTrue(wrappedMonitor.isDataStall()); + } + + @Test + public void testIsDataStall_EvaluationDnsOnMeteredNetwork() { + WrappedNetworkMonitor wrappedMonitor = makeMeteredWrappedNetworkMonitor(); + wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 100); + assertFalse(wrappedMonitor.isDataStall()); + + wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 1000); + makeDnsTimeoutEvent(wrappedMonitor, 5); + assertTrue(wrappedMonitor.isDataStall()); + } + + @Test + public void testIsDataStall_EvaluationDnsWithDnsTimeoutCount() { + WrappedNetworkMonitor wrappedMonitor = makeMeteredWrappedNetworkMonitor(); + wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 1000); + makeDnsTimeoutEvent(wrappedMonitor, 3); + assertFalse(wrappedMonitor.isDataStall()); + // Reset consecutive timeout counts. + makeDnsSuccessEvent(wrappedMonitor, 1); + makeDnsTimeoutEvent(wrappedMonitor, 2); + assertFalse(wrappedMonitor.isDataStall()); + + makeDnsTimeoutEvent(wrappedMonitor, 3); + assertTrue(wrappedMonitor.isDataStall()); + + // Set the value to larger than the default dns log size. + setConsecutiveDnsTimeoutThreshold(51); + wrappedMonitor = makeMeteredWrappedNetworkMonitor(); + wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 1000); + makeDnsTimeoutEvent(wrappedMonitor, 50); + assertFalse(wrappedMonitor.isDataStall()); + + makeDnsTimeoutEvent(wrappedMonitor, 1); + assertTrue(wrappedMonitor.isDataStall()); + } + + @Test + public void testIsDataStall_EvaluationDnsWithDnsTimeThreshold() { + // Test dns events happened in valid dns time threshold. + WrappedNetworkMonitor wrappedMonitor = makeMeteredWrappedNetworkMonitor(); + wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 100); + makeDnsTimeoutEvent(wrappedMonitor, 5); + assertFalse(wrappedMonitor.isDataStall()); + wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 1000); + assertTrue(wrappedMonitor.isDataStall()); + + // Test dns events happened before valid dns time threshold. + setValidDataStallDnsTimeThreshold(0); + wrappedMonitor = makeMeteredWrappedNetworkMonitor(); + wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 100); + makeDnsTimeoutEvent(wrappedMonitor, 5); + assertFalse(wrappedMonitor.isDataStall()); + wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 1000); + assertFalse(wrappedMonitor.isDataStall()); + } + + @Test + public void testBrokenNetworkNotValidated() throws Exception { + setSslException(mHttpsConnection); + setStatus(mHttpConnection, 500); + setStatus(mFallbackConnection, 404); + when(mCm.getNetworkCapabilities(any())).thenReturn(METERED_CAPABILITIES); + + final NetworkMonitor nm = makeMonitor(); + nm.notifyNetworkConnected(); + + verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)) + .notifyNetworkTested(NETWORK_TEST_RESULT_INVALID, null); + } + + @Test + public void testNoInternetCapabilityValidated() throws Exception { + when(mCm.getNetworkCapabilities(any())).thenReturn(NO_INTERNET_CAPABILITIES); + + final NetworkMonitor nm = makeMonitor(); + nm.notifyNetworkConnected(); + + verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)) + .notifyNetworkTested(NETWORK_TEST_RESULT_VALID, null); + verify(mNetwork, never()).openConnection(any()); + } + + @Test + public void testLaunchCaptivePortalApp() throws Exception { + setSslException(mHttpsConnection); + setPortal302(mHttpConnection); + + final NetworkMonitor nm = makeMonitor(); + nm.notifyNetworkConnected(); + + verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)) + .showProvisioningNotification(any()); + + // Check that startCaptivePortalApp sends the expected intent. + nm.launchCaptivePortalApp(); + + final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(mContext, timeout(HANDLER_TIMEOUT_MS).times(1)) + .startActivityAsUser(intentCaptor.capture(), eq(UserHandle.CURRENT)); + final Intent intent = intentCaptor.getValue(); + assertEquals(ACTION_CAPTIVE_PORTAL_SIGN_IN, intent.getAction()); + final Network network = intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK); + assertEquals(TEST_NETID, network.netId); + + // Have the app report that the captive portal is dismissed, and check that we revalidate. + setStatus(mHttpsConnection, 204); + setStatus(mHttpConnection, 204); + final CaptivePortal captivePortal = intent.getParcelableExtra(EXTRA_CAPTIVE_PORTAL); + captivePortal.reportCaptivePortalDismissed(); + verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)) + .notifyNetworkTested(NETWORK_TEST_RESULT_VALID, null); + } + + private void makeDnsTimeoutEvent(WrappedNetworkMonitor wrappedMonitor, int count) { + for (int i = 0; i < count; i++) { + wrappedMonitor.getDnsStallDetector().accumulateConsecutiveDnsTimeoutCount( + RETURN_CODE_DNS_TIMEOUT); + } + } + + private void makeDnsSuccessEvent(WrappedNetworkMonitor wrappedMonitor, int count) { + for (int i = 0; i < count; i++) { + wrappedMonitor.getDnsStallDetector().accumulateConsecutiveDnsTimeoutCount( + RETURN_CODE_DNS_SUCCESS); + } + } + + private void setDataStallEvaluationType(int type) { + when(mDependencies.getSetting(any(), + eq(Settings.Global.DATA_STALL_EVALUATION_TYPE), anyInt())).thenReturn(type); + } + + private void setMinDataStallEvaluateInterval(int time) { + when(mDependencies.getSetting(any(), + eq(Settings.Global.DATA_STALL_MIN_EVALUATE_INTERVAL), anyInt())).thenReturn(time); + } + + private void setValidDataStallDnsTimeThreshold(int time) { + when(mDependencies.getSetting(any(), + eq(Settings.Global.DATA_STALL_VALID_DNS_TIME_THRESHOLD), anyInt())).thenReturn(time); + } + + private void setConsecutiveDnsTimeoutThreshold(int num) { + when(mDependencies.getSetting(any(), + eq(Settings.Global.DATA_STALL_CONSECUTIVE_DNS_TIMEOUT_THRESHOLD), anyInt())) + .thenReturn(num); + } + + private void setFallbackUrl(String url) { + when(mDependencies.getSetting(any(), + eq(Settings.Global.CAPTIVE_PORTAL_FALLBACK_URL), any())).thenReturn(url); + } + + private void setOtherFallbackUrls(String urls) { + when(mDependencies.getSetting(any(), + eq(Settings.Global.CAPTIVE_PORTAL_OTHER_FALLBACK_URLS), any())).thenReturn(urls); + } + + private void setFallbackSpecs(String specs) { + when(mDependencies.getSetting(any(), + eq(Settings.Global.CAPTIVE_PORTAL_FALLBACK_PROBE_SPECS), any())).thenReturn(specs); + } + + private void setCaptivePortalMode(int mode) { + when(mDependencies.getSetting(any(), + eq(Settings.Global.CAPTIVE_PORTAL_MODE), anyInt())).thenReturn(mode); + } + + private void assertPortal(CaptivePortalProbeResult result) { + assertTrue(result.isPortal()); + assertFalse(result.isFailed()); + assertFalse(result.isSuccessful()); + } + + private void assertNotPortal(CaptivePortalProbeResult result) { + assertFalse(result.isPortal()); + assertFalse(result.isFailed()); + assertTrue(result.isSuccessful()); + } + + private void assertFailed(CaptivePortalProbeResult result) { + assertFalse(result.isPortal()); + assertTrue(result.isFailed()); + assertFalse(result.isSuccessful()); + } + + private void setSslException(HttpURLConnection connection) throws IOException { + doThrow(new SSLHandshakeException("Invalid cert")).when(connection).getResponseCode(); + } + + private void set302(HttpURLConnection connection, String location) throws IOException { + setStatus(connection, 302); + doReturn(location).when(connection).getHeaderField(LOCATION_HEADER); + } + + private void setPortal302(HttpURLConnection connection) throws IOException { + set302(connection, "http://login.example.com"); + } + + private void setStatus(HttpURLConnection connection, int status) throws IOException { + doReturn(status).when(connection).getResponseCode(); + } +} + diff --git a/packages/NetworkStack/tests/src/com/android/server/util/SharedLogTest.java b/packages/NetworkStack/tests/src/com/android/server/util/SharedLogTest.java new file mode 100644 index 000000000000..07ad3123bc53 --- /dev/null +++ b/packages/NetworkStack/tests/src/com/android/server/util/SharedLogTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.net.util.SharedLog; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class SharedLogTest { + private static final String TIMESTAMP_PATTERN = "\\d{2}:\\d{2}:\\d{2}"; + private static final String TIMESTAMP = "HH:MM:SS"; + + @Test + public void testBasicOperation() { + final SharedLog logTop = new SharedLog("top"); + logTop.mark("first post!"); + + final SharedLog logLevel2a = logTop.forSubComponent("twoA"); + final SharedLog logLevel2b = logTop.forSubComponent("twoB"); + logLevel2b.e("2b or not 2b"); + logLevel2b.e("No exception", null); + logLevel2b.e("Wait, here's one", new Exception("Test")); + logLevel2a.w("second post?"); + + final SharedLog logLevel3 = logLevel2a.forSubComponent("three"); + logTop.log("still logging"); + logLevel3.log("3 >> 2"); + logLevel2a.mark("ok: last post"); + + final String[] expected = { + " - MARK first post!", + " - [twoB] ERROR 2b or not 2b", + " - [twoB] ERROR No exception", + // No stacktrace in shared log, only in logcat + " - [twoB] ERROR Wait, here's one: Test", + " - [twoA] WARN second post?", + " - still logging", + " - [twoA.three] 3 >> 2", + " - [twoA] MARK ok: last post", + }; + // Verify the logs are all there and in the correct order. + verifyLogLines(expected, logTop); + + // In fact, because they all share the same underlying LocalLog, + // every subcomponent SharedLog's dump() is identical. + verifyLogLines(expected, logLevel2a); + verifyLogLines(expected, logLevel2b); + verifyLogLines(expected, logLevel3); + } + + private static void verifyLogLines(String[] expected, SharedLog log) { + final ByteArrayOutputStream ostream = new ByteArrayOutputStream(); + final PrintWriter pw = new PrintWriter(ostream, true); + log.dump(null, pw, null); + + final String dumpOutput = ostream.toString(); + assertTrue(dumpOutput != null); + assertTrue(!"".equals(dumpOutput)); + + final String[] lines = dumpOutput.split("\n"); + assertEquals(expected.length, lines.length); + + for (int i = 0; i < expected.length; i++) { + String got = lines[i]; + String want = expected[i]; + assertTrue(String.format("'%s' did not contain '%s'", got, want), got.endsWith(want)); + assertTrue(String.format("'%s' did not contain a %s timestamp", got, TIMESTAMP), + got.replaceFirst(TIMESTAMP_PATTERN, TIMESTAMP).contains(TIMESTAMP)); + } + } +} |