diff --git a/tests/OWNERS b/tests/OWNERS new file mode 100644 index 0000000000..d3836d4c6c --- /dev/null +++ b/tests/OWNERS @@ -0,0 +1,8 @@ +set noparent + +codewiz@google.com +jchalard@google.com +junyulai@google.com +lorenzo@google.com +reminv@google.com +satk@google.com diff --git a/tests/TEST_MAPPING b/tests/TEST_MAPPING new file mode 100644 index 0000000000..502f885ceb --- /dev/null +++ b/tests/TEST_MAPPING @@ -0,0 +1,34 @@ +{ + "presubmit": [ + { + "name": "FrameworksNetIntegrationTests" + } + ], + "postsubmit": [ + { + "name": "FrameworksNetDeflakeTest" + } + ], + "auto-postsubmit": [ + // Test tag for automotive targets. These are only running in postsubmit so as to harden the + // automotive targets to avoid introducing additional test flake and build time. The plan for + // presubmit testing for auto is to augment the existing tests to cover auto use cases as well. + // Additionally, this tag is used in targeted test suites to limit resource usage on the test + // infra during the hardening phase. + // TODO: this tag to be removed once the above is no longer an issue. + { + "name": "FrameworksNetTests" + }, + { + "name": "FrameworksNetIntegrationTests" + }, + { + "name": "FrameworksNetDeflakeTest" + } + ], + "imports": [ + { + "path": "packages/modules/Connectivity" + } + ] +} \ No newline at end of file diff --git a/tests/common/Android.bp b/tests/common/Android.bp new file mode 100644 index 0000000000..babb81c5fa --- /dev/null +++ b/tests/common/Android.bp @@ -0,0 +1,44 @@ +// +// 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. +// + +// Tests in this folder are included both in unit tests and CTS. +// They must be fast and stable, and exercise public or test APIs. +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library { + name: "FrameworksNetCommonTests", + srcs: ["java/**/*.java", "java/**/*.kt"], + static_libs: [ + "androidx.core_core", + "androidx.test.rules", + "junit", + "mockito-target-minus-junit4", + "modules-utils-build", + "net-tests-utils", + "net-utils-framework-common", + "platform-test-annotations", + ], + libs: [ + "android.test.base.stubs", + ], +} diff --git a/tests/common/java/ParseExceptionTest.kt b/tests/common/java/ParseExceptionTest.kt new file mode 100644 index 0000000000..b702d61a9f --- /dev/null +++ b/tests/common/java/ParseExceptionTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2021 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. + */ + +import android.net.ParseException +import android.os.Build +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.testutils.DevSdkIgnoreRule +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertNull +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ParseExceptionTest { + @get:Rule + val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.R) + + @Test + fun testConstructor_WithCause() { + val testMessage = "Test message" + val base = Exception("Test") + val exception = ParseException(testMessage, base) + + assertEquals(testMessage, exception.response) + assertEquals(base, exception.cause) + } + + @Test + fun testConstructor_NoCause() { + val testMessage = "Test message" + val exception = ParseException(testMessage) + + assertEquals(testMessage, exception.response) + assertNull(exception.cause) + } +} \ No newline at end of file diff --git a/tests/common/java/android/net/CaptivePortalDataTest.kt b/tests/common/java/android/net/CaptivePortalDataTest.kt new file mode 100644 index 0000000000..18a93319b2 --- /dev/null +++ b/tests/common/java/android/net/CaptivePortalDataTest.kt @@ -0,0 +1,190 @@ +/* + * 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 + +import android.os.Build +import androidx.test.filters.SmallTest +import com.android.modules.utils.build.SdkLevel +import com.android.testutils.assertParcelSane +import com.android.testutils.assertParcelingIsLossless +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import com.android.testutils.DevSdkIgnoreRunner +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +@SmallTest +@RunWith(DevSdkIgnoreRunner::class) +@IgnoreUpTo(Build.VERSION_CODES.Q) +class CaptivePortalDataTest { + @Rule @JvmField + val ignoreRule = DevSdkIgnoreRule() + + private val data = CaptivePortalData.Builder() + .setRefreshTime(123L) + .setUserPortalUrl(Uri.parse("https://portal.example.com/test")) + .setVenueInfoUrl(Uri.parse("https://venue.example.com/test")) + .setSessionExtendable(true) + .setBytesRemaining(456L) + .setExpiryTime(789L) + .setCaptive(true) + .apply { + if (SdkLevel.isAtLeastS()) { + setVenueFriendlyName("venue friendly name") + } + } + .build() + + private val dataFromPasspoint = CaptivePortalData.Builder() + .setCaptive(true) + .apply { + if (SdkLevel.isAtLeastS()) { + setVenueFriendlyName("venue friendly name") + setUserPortalUrl(Uri.parse("https://tc.example.com/passpoint"), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) + setVenueInfoUrl(Uri.parse("https://venue.example.com/passpoint"), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) + } + } + .build() + + private fun makeBuilder() = CaptivePortalData.Builder(data) + + @Test + fun testParcelUnparcel() { + val fieldCount = if (SdkLevel.isAtLeastS()) 10 else 7 + assertParcelSane(data, fieldCount) + assertParcelSane(dataFromPasspoint, fieldCount) + + assertParcelingIsLossless(makeBuilder().setUserPortalUrl(null).build()) + assertParcelingIsLossless(makeBuilder().setVenueInfoUrl(null).build()) + } + + @Test + fun testEquals() { + assertEquals(data, makeBuilder().build()) + + assertNotEqualsAfterChange { it.setRefreshTime(456L) } + assertNotEqualsAfterChange { it.setUserPortalUrl(Uri.parse("https://example.com/")) } + assertNotEqualsAfterChange { it.setUserPortalUrl(null) } + assertNotEqualsAfterChange { it.setVenueInfoUrl(Uri.parse("https://example.com/")) } + assertNotEqualsAfterChange { it.setVenueInfoUrl(null) } + assertNotEqualsAfterChange { it.setSessionExtendable(false) } + assertNotEqualsAfterChange { it.setBytesRemaining(789L) } + assertNotEqualsAfterChange { it.setExpiryTime(12L) } + assertNotEqualsAfterChange { it.setCaptive(false) } + + if (SdkLevel.isAtLeastS()) { + assertNotEqualsAfterChange { it.setVenueFriendlyName("another friendly name") } + assertNotEqualsAfterChange { it.setVenueFriendlyName(null) } + + assertEquals(dataFromPasspoint, CaptivePortalData.Builder(dataFromPasspoint).build()) + assertNotEqualsAfterChange { it.setUserPortalUrl( + Uri.parse("https://tc.example.com/passpoint")) } + assertNotEqualsAfterChange { it.setUserPortalUrl( + Uri.parse("https://tc.example.com/passpoint"), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER) } + assertNotEqualsAfterChange { it.setUserPortalUrl( + Uri.parse("https://tc.example.com/other"), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) } + assertNotEqualsAfterChange { it.setUserPortalUrl( + Uri.parse("https://tc.example.com/passpoint"), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER) } + assertNotEqualsAfterChange { it.setVenueInfoUrl( + Uri.parse("https://venue.example.com/passpoint")) } + assertNotEqualsAfterChange { it.setVenueInfoUrl( + Uri.parse("https://venue.example.com/other"), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) } + assertNotEqualsAfterChange { it.setVenueInfoUrl( + Uri.parse("https://venue.example.com/passpoint"), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER) } + } + } + + @Test + fun testUserPortalUrl() { + assertEquals(Uri.parse("https://portal.example.com/test"), data.userPortalUrl) + } + + @Test + fun testVenueInfoUrl() { + assertEquals(Uri.parse("https://venue.example.com/test"), data.venueInfoUrl) + } + + @Test + fun testIsSessionExtendable() { + assertTrue(data.isSessionExtendable) + } + + @Test + fun testByteLimit() { + assertEquals(456L, data.byteLimit) + // Test byteLimit unset. + assertEquals(-1L, CaptivePortalData.Builder(null).build().byteLimit) + } + + @Test + fun testRefreshTimeMillis() { + assertEquals(123L, data.refreshTimeMillis) + } + + @Test + fun testExpiryTimeMillis() { + assertEquals(789L, data.expiryTimeMillis) + // Test expiryTimeMillis unset. + assertEquals(-1L, CaptivePortalData.Builder(null).build().expiryTimeMillis) + } + + @Test + fun testIsCaptive() { + assertTrue(data.isCaptive) + assertFalse(makeBuilder().setCaptive(false).build().isCaptive) + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + fun testVenueFriendlyName() { + assertEquals("venue friendly name", data.venueFriendlyName) + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + fun testGetVenueInfoUrlSource() { + assertEquals(CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER, + data.venueInfoUrlSource) + assertEquals(CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT, + dataFromPasspoint.venueInfoUrlSource) + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + fun testGetUserPortalUrlSource() { + assertEquals(CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER, + data.userPortalUrlSource) + assertEquals(CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT, + dataFromPasspoint.userPortalUrlSource) + } + + private fun CaptivePortalData.mutate(mutator: (CaptivePortalData.Builder) -> Unit) = + CaptivePortalData.Builder(this).apply { mutator(this) }.build() + + private fun assertNotEqualsAfterChange(mutator: (CaptivePortalData.Builder) -> Unit) { + assertNotEquals(data, data.mutate(mutator)) + } +} \ No newline at end of file diff --git a/tests/common/java/android/net/CaptivePortalTest.java b/tests/common/java/android/net/CaptivePortalTest.java new file mode 100644 index 0000000000..15d3398d43 --- /dev/null +++ b/tests/common/java/android/net/CaptivePortalTest.java @@ -0,0 +1,121 @@ +/* + * 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; + +import static org.junit.Assert.assertEquals; + +import android.os.Build; +import android.os.RemoteException; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class CaptivePortalTest { + @Rule + public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(); + + private static final int DEFAULT_TIMEOUT_MS = 5000; + private static final String TEST_PACKAGE_NAME = "com.google.android.test"; + + private final class MyCaptivePortalImpl extends ICaptivePortal.Stub { + int mCode = -1; + String mPackageName = null; + + @Override + public void appResponse(final int response) throws RemoteException { + mCode = response; + } + + @Override + public void appRequest(final int request) throws RemoteException { + mCode = request; + } + + // This is only @Override on R- + public void logEvent(int eventId, String packageName) throws RemoteException { + mCode = eventId; + mPackageName = packageName; + } + } + + private interface TestFunctor { + void useCaptivePortal(CaptivePortal o); + } + + private MyCaptivePortalImpl runCaptivePortalTest(TestFunctor f) { + final MyCaptivePortalImpl cp = new MyCaptivePortalImpl(); + f.useCaptivePortal(new CaptivePortal(cp.asBinder())); + return cp; + } + + @Test + public void testReportCaptivePortalDismissed() { + final MyCaptivePortalImpl result = + runCaptivePortalTest(c -> c.reportCaptivePortalDismissed()); + assertEquals(result.mCode, CaptivePortal.APP_RETURN_DISMISSED); + } + + @Test + public void testIgnoreNetwork() { + final MyCaptivePortalImpl result = runCaptivePortalTest(c -> c.ignoreNetwork()); + assertEquals(result.mCode, CaptivePortal.APP_RETURN_UNWANTED); + } + + @Test + public void testUseNetwork() { + final MyCaptivePortalImpl result = runCaptivePortalTest(c -> c.useNetwork()); + assertEquals(result.mCode, CaptivePortal.APP_RETURN_WANTED_AS_IS); + } + + @IgnoreUpTo(Build.VERSION_CODES.Q) + @Test + public void testReevaluateNetwork() { + final MyCaptivePortalImpl result = runCaptivePortalTest(c -> c.reevaluateNetwork()); + assertEquals(result.mCode, CaptivePortal.APP_REQUEST_REEVALUATION_REQUIRED); + } + + @IgnoreUpTo(Build.VERSION_CODES.R) + @Test + public void testLogEvent() { + /** + * From S testLogEvent is expected to do nothing but shouldn't crash (the API + * logEvent has been deprecated). + */ + final MyCaptivePortalImpl result = runCaptivePortalTest(c -> c.logEvent( + 0, + TEST_PACKAGE_NAME)); + } + + @IgnoreAfter(Build.VERSION_CODES.R) + @Test + public void testLogEvent_UntilR() { + final MyCaptivePortalImpl result = runCaptivePortalTest(c -> c.logEvent( + 42, TEST_PACKAGE_NAME)); + assertEquals(result.mCode, 42); + assertEquals(result.mPackageName, TEST_PACKAGE_NAME); + } +} diff --git a/tests/common/java/android/net/DependenciesTest.java b/tests/common/java/android/net/DependenciesTest.java new file mode 100644 index 0000000000..ac1c28a454 --- /dev/null +++ b/tests/common/java/android/net/DependenciesTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2020 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; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +/** + * A simple class that tests dependencies to java standard tools from the + * Network stack. These tests are not meant to be comprehensive tests of + * the relevant APIs : such tests belong in the relevant test suite for + * these dependencies. Instead, this just makes sure coverage is present + * by calling the methods in the exact way (or a representative way of how) + * they are called in the network stack. + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class DependenciesTest { + // Used to in ipmemorystore's RegularMaintenanceJobService to convert + // 24 hours into seconds + @Test + public void testTimeUnit() { + final int hours = 24; + final long inSeconds = TimeUnit.HOURS.toMillis(hours); + assertEquals(inSeconds, hours * 60 * 60 * 1000); + } + + private byte[] makeTrivialArray(final int size) { + final byte[] src = new byte[size]; + for (int i = 0; i < size; ++i) { + src[i] = (byte) i; + } + return src; + } + + // Used in ApfFilter to find an IP address from a byte array + @Test + public void testArrays() { + final int size = 128; + final byte[] src = makeTrivialArray(size); + + // Test copy + final int copySize = 16; + final int offset = 24; + final byte[] expected = new byte[copySize]; + for (int i = 0; i < copySize; ++i) { + expected[i] = (byte) (offset + i); + } + + final byte[] copy = Arrays.copyOfRange(src, offset, offset + copySize); + assertArrayEquals(expected, copy); + assertArrayEquals(new byte[0], Arrays.copyOfRange(src, size, size)); + } + + // Used mainly in the Dhcp code + @Test + public void testCopyOf() { + final byte[] src = makeTrivialArray(128); + final byte[] copy = Arrays.copyOf(src, src.length); + assertArrayEquals(src, copy); + assertFalse(src == copy); + + assertArrayEquals(new byte[0], Arrays.copyOf(src, 0)); + + final int excess = 16; + final byte[] biggerCopy = Arrays.copyOf(src, src.length + excess); + for (int i = src.length; i < src.length + excess; ++i) { + assertEquals(0, biggerCopy[i]); + } + for (int i = src.length - 1; i >= 0; --i) { + assertEquals(src[i], biggerCopy[i]); + } + } + + // Used mainly in DnsUtils but also various other places + @Test + public void testAsList() { + final int size = 24; + final Object[] src = new Object[size]; + final ArrayList expected = new ArrayList<>(size); + for (int i = 0; i < size; ++i) { + final Object o = new Object(); + src[i] = o; + expected.add(o); + } + assertEquals(expected, Arrays.asList(src)); + } +} diff --git a/tests/common/java/android/net/DhcpInfoTest.java b/tests/common/java/android/net/DhcpInfoTest.java new file mode 100644 index 0000000000..ab4726bab5 --- /dev/null +++ b/tests/common/java/android/net/DhcpInfoTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2009 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; + +import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTL; +import static com.android.testutils.MiscAsserts.assertFieldCountEquals; +import static com.android.testutils.ParcelUtils.parcelingRoundTrip; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.annotation.Nullable; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.Inet4Address; +import java.net.InetAddress; + +@RunWith(AndroidJUnit4.class) +public class DhcpInfoTest { + private static final String STR_ADDR1 = "255.255.255.255"; + private static final String STR_ADDR2 = "127.0.0.1"; + private static final String STR_ADDR3 = "192.168.1.1"; + private static final String STR_ADDR4 = "192.168.1.0"; + private static final int LEASE_TIME = 9999; + + private int ipToInteger(String ipString) throws Exception { + return inet4AddressToIntHTL((Inet4Address) InetAddress.getByName(ipString)); + } + + private DhcpInfo createDhcpInfoObject() throws Exception { + final DhcpInfo dhcpInfo = new DhcpInfo(); + dhcpInfo.ipAddress = ipToInteger(STR_ADDR1); + dhcpInfo.gateway = ipToInteger(STR_ADDR2); + dhcpInfo.netmask = ipToInteger(STR_ADDR3); + dhcpInfo.dns1 = ipToInteger(STR_ADDR4); + dhcpInfo.dns2 = ipToInteger(STR_ADDR4); + dhcpInfo.serverAddress = ipToInteger(STR_ADDR2); + dhcpInfo.leaseDuration = LEASE_TIME; + return dhcpInfo; + } + + @Test + public void testConstructor() { + new DhcpInfo(); + } + + @Test + public void testToString() throws Exception { + final String expectedDefault = "ipaddr 0.0.0.0 gateway 0.0.0.0 netmask 0.0.0.0 " + + "dns1 0.0.0.0 dns2 0.0.0.0 DHCP server 0.0.0.0 lease 0 seconds"; + + DhcpInfo dhcpInfo = new DhcpInfo(); + + // Test default string. + assertEquals(expectedDefault, dhcpInfo.toString()); + + dhcpInfo = createDhcpInfoObject(); + + final String expected = "ipaddr " + STR_ADDR1 + " gateway " + STR_ADDR2 + " netmask " + + STR_ADDR3 + " dns1 " + STR_ADDR4 + " dns2 " + STR_ADDR4 + " DHCP server " + + STR_ADDR2 + " lease " + LEASE_TIME + " seconds"; + // Test with new values + assertEquals(expected, dhcpInfo.toString()); + } + + private boolean dhcpInfoEquals(@Nullable DhcpInfo left, @Nullable DhcpInfo right) { + if (left == null && right == null) return true; + + if (left == null || right == null) return false; + + return left.ipAddress == right.ipAddress + && left.gateway == right.gateway + && left.netmask == right.netmask + && left.dns1 == right.dns1 + && left.dns2 == right.dns2 + && left.serverAddress == right.serverAddress + && left.leaseDuration == right.leaseDuration; + } + + @Test + public void testParcelDhcpInfo() throws Exception { + // Cannot use assertParcelSane() here because this requires .equals() to work as + // defined, but DhcpInfo has a different legacy behavior that we cannot change. + final DhcpInfo dhcpInfo = createDhcpInfoObject(); + assertFieldCountEquals(7, DhcpInfo.class); + + final DhcpInfo dhcpInfoRoundTrip = parcelingRoundTrip(dhcpInfo); + assertTrue(dhcpInfoEquals(null, null)); + assertFalse(dhcpInfoEquals(null, dhcpInfoRoundTrip)); + assertFalse(dhcpInfoEquals(dhcpInfo, null)); + assertTrue(dhcpInfoEquals(dhcpInfo, dhcpInfoRoundTrip)); + } +} diff --git a/tests/common/java/android/net/IpPrefixTest.java b/tests/common/java/android/net/IpPrefixTest.java new file mode 100644 index 0000000000..50ecb42835 --- /dev/null +++ b/tests/common/java/android/net/IpPrefixTest.java @@ -0,0 +1,374 @@ +/* + * Copyright (C) 2014 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; + +import static com.android.testutils.MiscAsserts.assertEqualBothWays; +import static com.android.testutils.MiscAsserts.assertFieldCountEquals; +import static com.android.testutils.MiscAsserts.assertNotEqualEitherWay; +import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; + +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.assertTrue; +import static org.junit.Assert.fail; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.InetAddress; +import java.util.Random; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class IpPrefixTest { + + private static InetAddress address(String addr) { + return InetAddress.parseNumericAddress(addr); + } + + // Explicitly cast everything to byte because "error: possible loss of precision". + private static final byte[] IPV4_BYTES = { (byte) 192, (byte) 0, (byte) 2, (byte) 4}; + private static final byte[] IPV6_BYTES = { + (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8, + (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef, + (byte) 0x0f, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xa0 + }; + + @Test + public void testConstructor() { + IpPrefix p; + try { + p = new IpPrefix((byte[]) null, 9); + fail("Expected NullPointerException: null byte array"); + } catch (RuntimeException expected) { } + + try { + p = new IpPrefix((InetAddress) null, 10); + fail("Expected NullPointerException: null InetAddress"); + } catch (RuntimeException expected) { } + + try { + p = new IpPrefix((String) null); + fail("Expected NullPointerException: null String"); + } catch (RuntimeException expected) { } + + + try { + byte[] b2 = {1, 2, 3, 4, 5}; + p = new IpPrefix(b2, 29); + fail("Expected IllegalArgumentException: invalid array length"); + } catch (IllegalArgumentException expected) { } + + try { + p = new IpPrefix("1.2.3.4"); + fail("Expected IllegalArgumentException: no prefix length"); + } catch (IllegalArgumentException expected) { } + + try { + p = new IpPrefix("1.2.3.4/"); + fail("Expected IllegalArgumentException: empty prefix length"); + } catch (IllegalArgumentException expected) { } + + try { + p = new IpPrefix("foo/32"); + fail("Expected IllegalArgumentException: invalid address"); + } catch (IllegalArgumentException expected) { } + + try { + p = new IpPrefix("1/32"); + fail("Expected IllegalArgumentException: deprecated IPv4 format"); + } catch (IllegalArgumentException expected) { } + + try { + p = new IpPrefix("1.2.3.256/32"); + fail("Expected IllegalArgumentException: invalid IPv4 address"); + } catch (IllegalArgumentException expected) { } + + try { + p = new IpPrefix("foo/32"); + fail("Expected IllegalArgumentException: non-address"); + } catch (IllegalArgumentException expected) { } + + try { + p = new IpPrefix("f00:::/32"); + fail("Expected IllegalArgumentException: invalid IPv6 address"); + } catch (IllegalArgumentException expected) { } + + p = new IpPrefix("/64"); + assertEquals("::/64", p.toString()); + + p = new IpPrefix("/128"); + assertEquals("::1/128", p.toString()); + + p = new IpPrefix("[2001:db8::123]/64"); + assertEquals("2001:db8::/64", p.toString()); + } + + @Test + public void testTruncation() { + IpPrefix p; + + p = new IpPrefix(IPV4_BYTES, 32); + assertEquals("192.0.2.4/32", p.toString()); + + p = new IpPrefix(IPV4_BYTES, 29); + assertEquals("192.0.2.0/29", p.toString()); + + p = new IpPrefix(IPV4_BYTES, 8); + assertEquals("192.0.0.0/8", p.toString()); + + p = new IpPrefix(IPV4_BYTES, 0); + assertEquals("0.0.0.0/0", p.toString()); + + try { + p = new IpPrefix(IPV4_BYTES, 33); + fail("Expected IllegalArgumentException: invalid prefix length"); + } catch (RuntimeException expected) { } + + try { + p = new IpPrefix(IPV4_BYTES, 128); + fail("Expected IllegalArgumentException: invalid prefix length"); + } catch (RuntimeException expected) { } + + try { + p = new IpPrefix(IPV4_BYTES, -1); + fail("Expected IllegalArgumentException: negative prefix length"); + } catch (RuntimeException expected) { } + + p = new IpPrefix(IPV6_BYTES, 128); + assertEquals("2001:db8:dead:beef:f00::a0/128", p.toString()); + + p = new IpPrefix(IPV6_BYTES, 122); + assertEquals("2001:db8:dead:beef:f00::80/122", p.toString()); + + p = new IpPrefix(IPV6_BYTES, 64); + assertEquals("2001:db8:dead:beef::/64", p.toString()); + + p = new IpPrefix(IPV6_BYTES, 3); + assertEquals("2000::/3", p.toString()); + + p = new IpPrefix(IPV6_BYTES, 0); + assertEquals("::/0", p.toString()); + + try { + p = new IpPrefix(IPV6_BYTES, -1); + fail("Expected IllegalArgumentException: negative prefix length"); + } catch (RuntimeException expected) { } + + try { + p = new IpPrefix(IPV6_BYTES, 129); + fail("Expected IllegalArgumentException: negative prefix length"); + } catch (RuntimeException expected) { } + + } + + @Test + public void testEquals() { + IpPrefix p1, p2; + + p1 = new IpPrefix("192.0.2.251/23"); + p2 = new IpPrefix(new byte[]{(byte) 192, (byte) 0, (byte) 2, (byte) 251}, 23); + assertEqualBothWays(p1, p2); + + p1 = new IpPrefix("192.0.2.5/23"); + assertEqualBothWays(p1, p2); + + p1 = new IpPrefix("192.0.2.5/24"); + assertNotEqualEitherWay(p1, p2); + + p1 = new IpPrefix("192.0.4.5/23"); + assertNotEqualEitherWay(p1, p2); + + + p1 = new IpPrefix("2001:db8:dead:beef:f00::80/122"); + p2 = new IpPrefix(IPV6_BYTES, 122); + assertEquals("2001:db8:dead:beef:f00::80/122", p2.toString()); + assertEqualBothWays(p1, p2); + + p1 = new IpPrefix("2001:db8:dead:beef:f00::bf/122"); + assertEqualBothWays(p1, p2); + + p1 = new IpPrefix("2001:db8:dead:beef:f00::8:0/123"); + assertNotEqualEitherWay(p1, p2); + + p1 = new IpPrefix("2001:db8:dead:beef::/122"); + assertNotEqualEitherWay(p1, p2); + + // 192.0.2.4/32 != c000:0204::/32. + byte[] ipv6bytes = new byte[16]; + System.arraycopy(IPV4_BYTES, 0, ipv6bytes, 0, IPV4_BYTES.length); + p1 = new IpPrefix(ipv6bytes, 32); + assertEqualBothWays(p1, new IpPrefix("c000:0204::/32")); + + p2 = new IpPrefix(IPV4_BYTES, 32); + assertNotEqualEitherWay(p1, p2); + } + + @Test + public void testContainsInetAddress() { + IpPrefix p = new IpPrefix("2001:db8:f00::ace:d00d/127"); + assertTrue(p.contains(address("2001:db8:f00::ace:d00c"))); + assertTrue(p.contains(address("2001:db8:f00::ace:d00d"))); + assertFalse(p.contains(address("2001:db8:f00::ace:d00e"))); + assertFalse(p.contains(address("2001:db8:f00::bad:d00d"))); + assertFalse(p.contains(address("2001:4868:4860::8888"))); + assertFalse(p.contains(address("8.8.8.8"))); + + p = new IpPrefix("192.0.2.0/23"); + assertTrue(p.contains(address("192.0.2.43"))); + assertTrue(p.contains(address("192.0.3.21"))); + assertFalse(p.contains(address("192.0.0.21"))); + assertFalse(p.contains(address("8.8.8.8"))); + assertFalse(p.contains(address("2001:4868:4860::8888"))); + + IpPrefix ipv6Default = new IpPrefix("::/0"); + assertTrue(ipv6Default.contains(address("2001:db8::f00"))); + assertFalse(ipv6Default.contains(address("192.0.2.1"))); + + IpPrefix ipv4Default = new IpPrefix("0.0.0.0/0"); + assertTrue(ipv4Default.contains(address("255.255.255.255"))); + assertTrue(ipv4Default.contains(address("192.0.2.1"))); + assertFalse(ipv4Default.contains(address("2001:db8::f00"))); + } + + @Test + public void testContainsIpPrefix() { + assertTrue(new IpPrefix("0.0.0.0/0").containsPrefix(new IpPrefix("0.0.0.0/0"))); + assertTrue(new IpPrefix("0.0.0.0/0").containsPrefix(new IpPrefix("1.2.3.4/0"))); + assertTrue(new IpPrefix("0.0.0.0/0").containsPrefix(new IpPrefix("1.2.3.4/8"))); + assertTrue(new IpPrefix("0.0.0.0/0").containsPrefix(new IpPrefix("1.2.3.4/24"))); + assertTrue(new IpPrefix("0.0.0.0/0").containsPrefix(new IpPrefix("1.2.3.4/23"))); + + assertTrue(new IpPrefix("1.2.3.4/8").containsPrefix(new IpPrefix("1.2.3.4/8"))); + assertTrue(new IpPrefix("1.2.3.4/8").containsPrefix(new IpPrefix("1.254.12.9/8"))); + assertTrue(new IpPrefix("1.2.3.4/21").containsPrefix(new IpPrefix("1.2.3.4/21"))); + assertTrue(new IpPrefix("1.2.3.4/32").containsPrefix(new IpPrefix("1.2.3.4/32"))); + + assertTrue(new IpPrefix("1.2.3.4/20").containsPrefix(new IpPrefix("1.2.3.0/24"))); + + assertFalse(new IpPrefix("1.2.3.4/32").containsPrefix(new IpPrefix("1.2.3.5/32"))); + assertFalse(new IpPrefix("1.2.3.4/8").containsPrefix(new IpPrefix("2.2.3.4/8"))); + assertFalse(new IpPrefix("0.0.0.0/16").containsPrefix(new IpPrefix("0.0.0.0/15"))); + assertFalse(new IpPrefix("100.0.0.0/8").containsPrefix(new IpPrefix("99.0.0.0/8"))); + + assertTrue(new IpPrefix("::/0").containsPrefix(new IpPrefix("::/0"))); + assertTrue(new IpPrefix("::/0").containsPrefix(new IpPrefix("2001:db8::f00/1"))); + assertTrue(new IpPrefix("::/0").containsPrefix(new IpPrefix("3d8a:661:a0::770/8"))); + assertTrue(new IpPrefix("::/0").containsPrefix(new IpPrefix("2001:db8::f00/8"))); + assertTrue(new IpPrefix("::/0").containsPrefix(new IpPrefix("2001:db8::f00/64"))); + assertTrue(new IpPrefix("::/0").containsPrefix(new IpPrefix("2001:db8::f00/113"))); + assertTrue(new IpPrefix("::/0").containsPrefix(new IpPrefix("2001:db8::f00/128"))); + + assertTrue(new IpPrefix("2001:db8:f00::ace:d00d/64").containsPrefix( + new IpPrefix("2001:db8:f00::ace:d00d/64"))); + assertTrue(new IpPrefix("2001:db8:f00::ace:d00d/64").containsPrefix( + new IpPrefix("2001:db8:f00::ace:d00d/120"))); + assertFalse(new IpPrefix("2001:db8:f00::ace:d00d/64").containsPrefix( + new IpPrefix("2001:db8:f00::ace:d00d/32"))); + assertFalse(new IpPrefix("2001:db8:f00::ace:d00d/64").containsPrefix( + new IpPrefix("2006:db8:f00::ace:d00d/96"))); + + assertTrue(new IpPrefix("2001:db8:f00::ace:d00d/128").containsPrefix( + new IpPrefix("2001:db8:f00::ace:d00d/128"))); + assertTrue(new IpPrefix("2001:db8:f00::ace:d00d/100").containsPrefix( + new IpPrefix("2001:db8:f00::ace:ccaf/110"))); + + assertFalse(new IpPrefix("2001:db8:f00::ace:d00d/128").containsPrefix( + new IpPrefix("2001:db8:f00::ace:d00e/128"))); + assertFalse(new IpPrefix("::/30").containsPrefix(new IpPrefix("::/29"))); + } + + @Test + public void testHashCode() { + IpPrefix p = new IpPrefix(new byte[4], 0); + Random random = new Random(); + for (int i = 0; i < 100; i++) { + final IpPrefix oldP = p; + if (random.nextBoolean()) { + // IPv4. + byte[] b = new byte[4]; + random.nextBytes(b); + p = new IpPrefix(b, random.nextInt(33)); + } else { + // IPv6. + byte[] b = new byte[16]; + random.nextBytes(b); + p = new IpPrefix(b, random.nextInt(129)); + } + if (p.equals(oldP)) { + assertEquals(p.hashCode(), oldP.hashCode()); + } + if (p.hashCode() != oldP.hashCode()) { + assertNotEquals(p, oldP); + } + } + } + + @Test + public void testHashCodeIsNotConstant() { + IpPrefix[] prefixes = { + new IpPrefix("2001:db8:f00::ace:d00d/127"), + new IpPrefix("192.0.2.0/23"), + new IpPrefix("::/0"), + new IpPrefix("0.0.0.0/0"), + }; + for (int i = 0; i < prefixes.length; i++) { + for (int j = i + 1; j < prefixes.length; j++) { + assertNotEquals(prefixes[i].hashCode(), prefixes[j].hashCode()); + } + } + } + + @Test + public void testMappedAddressesAreBroken() { + // 192.0.2.0/24 != ::ffff:c000:0204/120, but because we use InetAddress, + // we are unable to comprehend that. + byte[] ipv6bytes = { + (byte) 0, (byte) 0, (byte) 0, (byte) 0, + (byte) 0, (byte) 0, (byte) 0, (byte) 0, + (byte) 0, (byte) 0, (byte) 0xff, (byte) 0xff, + (byte) 192, (byte) 0, (byte) 2, (byte) 0}; + IpPrefix p = new IpPrefix(ipv6bytes, 120); + assertEquals(16, p.getRawAddress().length); // Fine. + assertArrayEquals(ipv6bytes, p.getRawAddress()); // Fine. + + // Broken. + assertEquals("192.0.2.0/120", p.toString()); + assertEquals(InetAddress.parseNumericAddress("192.0.2.0"), p.getAddress()); + } + + @Test + public void testParceling() { + IpPrefix p; + + p = new IpPrefix("2001:4860:db8::/64"); + assertParcelingIsLossless(p); + assertTrue(p.isIPv6()); + + p = new IpPrefix("192.0.2.0/25"); + assertParcelingIsLossless(p); + assertTrue(p.isIPv4()); + + assertFieldCountEquals(2, IpPrefix.class); + } +} diff --git a/tests/common/java/android/net/KeepalivePacketDataTest.kt b/tests/common/java/android/net/KeepalivePacketDataTest.kt new file mode 100644 index 0000000000..f464ec6cf0 --- /dev/null +++ b/tests/common/java/android/net/KeepalivePacketDataTest.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2020 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 + +import android.net.InvalidPacketException.ERROR_INVALID_IP_ADDRESS +import android.net.InvalidPacketException.ERROR_INVALID_PORT +import android.os.Build +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import java.net.InetAddress +import java.util.Arrays +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class KeepalivePacketDataTest { + @Rule @JvmField + val ignoreRule: DevSdkIgnoreRule = DevSdkIgnoreRule() + + private val INVALID_PORT = 65537 + private val TEST_DST_PORT = 4244 + private val TEST_SRC_PORT = 4243 + + private val TESTBYTES = byteArrayOf(12, 31, 22, 44) + private val TEST_SRC_ADDRV4 = "198.168.0.2".address() + private val TEST_DST_ADDRV4 = "198.168.0.1".address() + private val TEST_ADDRV6 = "2001:db8::1".address() + + private fun String.address() = InetAddresses.parseNumericAddress(this) + + // Add for test because constructor of KeepalivePacketData is protected. + private inner class TestKeepalivePacketData( + srcAddress: InetAddress? = TEST_SRC_ADDRV4, + srcPort: Int = TEST_SRC_PORT, + dstAddress: InetAddress? = TEST_DST_ADDRV4, + dstPort: Int = TEST_DST_PORT, + data: ByteArray = TESTBYTES + ) : KeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, data) + + @Test + @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testConstructor() { + var data: TestKeepalivePacketData + + try { + data = TestKeepalivePacketData(srcAddress = null) + fail("Null src address should cause exception") + } catch (e: InvalidPacketException) { + assertEquals(e.error, ERROR_INVALID_IP_ADDRESS) + } + + try { + data = TestKeepalivePacketData(dstAddress = null) + fail("Null dst address should cause exception") + } catch (e: InvalidPacketException) { + assertEquals(e.error, ERROR_INVALID_IP_ADDRESS) + } + + try { + data = TestKeepalivePacketData(dstAddress = TEST_ADDRV6) + fail("Ip family mismatched should cause exception") + } catch (e: InvalidPacketException) { + assertEquals(e.error, ERROR_INVALID_IP_ADDRESS) + } + + try { + data = TestKeepalivePacketData(srcPort = INVALID_PORT) + fail("Invalid srcPort should cause exception") + } catch (e: InvalidPacketException) { + assertEquals(e.error, ERROR_INVALID_PORT) + } + + try { + data = TestKeepalivePacketData(dstPort = INVALID_PORT) + fail("Invalid dstPort should cause exception") + } catch (e: InvalidPacketException) { + assertEquals(e.error, ERROR_INVALID_PORT) + } + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testSrcAddress() = assertEquals(TEST_SRC_ADDRV4, TestKeepalivePacketData().srcAddress) + + @Test + @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testDstAddress() = assertEquals(TEST_DST_ADDRV4, TestKeepalivePacketData().dstAddress) + + @Test + @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testSrcPort() = assertEquals(TEST_SRC_PORT, TestKeepalivePacketData().srcPort) + + @Test + @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testDstPort() = assertEquals(TEST_DST_PORT, TestKeepalivePacketData().dstPort) + + @Test + @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testPacket() = assertTrue(Arrays.equals(TESTBYTES, TestKeepalivePacketData().packet)) +} \ No newline at end of file diff --git a/tests/common/java/android/net/LinkAddressTest.java b/tests/common/java/android/net/LinkAddressTest.java new file mode 100644 index 0000000000..2cf3cf9c11 --- /dev/null +++ b/tests/common/java/android/net/LinkAddressTest.java @@ -0,0 +1,518 @@ +/* + * Copyright (C) 2013 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; + +import static android.system.OsConstants.IFA_F_DADFAILED; +import static android.system.OsConstants.IFA_F_DEPRECATED; +import static android.system.OsConstants.IFA_F_OPTIMISTIC; +import static android.system.OsConstants.IFA_F_PERMANENT; +import static android.system.OsConstants.IFA_F_TEMPORARY; +import static android.system.OsConstants.IFA_F_TENTATIVE; +import static android.system.OsConstants.RT_SCOPE_HOST; +import static android.system.OsConstants.RT_SCOPE_LINK; +import static android.system.OsConstants.RT_SCOPE_SITE; +import static android.system.OsConstants.RT_SCOPE_UNIVERSE; + +import static com.android.testutils.MiscAsserts.assertEqualBothWays; +import static com.android.testutils.MiscAsserts.assertFieldCountEquals; +import static com.android.testutils.MiscAsserts.assertNotEqualEitherWay; +import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.os.Build; +import android.os.SystemClock; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Arrays; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class LinkAddressTest { + @Rule + public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(); + + private static final String V4 = "192.0.2.1"; + private static final String V6 = "2001:db8::1"; + private static final InetAddress V4_ADDRESS = InetAddresses.parseNumericAddress(V4); + private static final InetAddress V6_ADDRESS = InetAddresses.parseNumericAddress(V6); + + @Test + public void testConstants() { + // RT_SCOPE_UNIVERSE = 0, but all the other constants should be nonzero. + assertNotEquals(0, RT_SCOPE_HOST); + assertNotEquals(0, RT_SCOPE_LINK); + assertNotEquals(0, RT_SCOPE_SITE); + + assertNotEquals(0, IFA_F_DEPRECATED); + assertNotEquals(0, IFA_F_PERMANENT); + assertNotEquals(0, IFA_F_TENTATIVE); + } + + @Test + public void testConstructors() throws SocketException { + LinkAddress address; + + // Valid addresses work as expected. + address = new LinkAddress(V4_ADDRESS, 25); + assertEquals(V4_ADDRESS, address.getAddress()); + assertEquals(25, address.getPrefixLength()); + assertEquals(0, address.getFlags()); + assertEquals(RT_SCOPE_UNIVERSE, address.getScope()); + assertTrue(address.isIpv4()); + + address = new LinkAddress(V6_ADDRESS, 127); + assertEquals(V6_ADDRESS, address.getAddress()); + assertEquals(127, address.getPrefixLength()); + assertEquals(0, address.getFlags()); + assertEquals(RT_SCOPE_UNIVERSE, address.getScope()); + assertTrue(address.isIpv6()); + + // Nonsensical flags/scopes or combinations thereof are acceptable. + address = new LinkAddress(V6 + "/64", IFA_F_DEPRECATED | IFA_F_PERMANENT, RT_SCOPE_LINK); + assertEquals(V6_ADDRESS, address.getAddress()); + assertEquals(64, address.getPrefixLength()); + assertEquals(IFA_F_DEPRECATED | IFA_F_PERMANENT, address.getFlags()); + assertEquals(RT_SCOPE_LINK, address.getScope()); + assertTrue(address.isIpv6()); + + address = new LinkAddress(V4 + "/23", 123, 456); + assertEquals(V4_ADDRESS, address.getAddress()); + assertEquals(23, address.getPrefixLength()); + assertEquals(123, address.getFlags()); + assertEquals(456, address.getScope()); + assertTrue(address.isIpv4()); + + address = new LinkAddress("/64", 1 /* flags */, 2 /* scope */); + assertEquals(Inet6Address.LOOPBACK, address.getAddress()); + assertEquals(64, address.getPrefixLength()); + assertEquals(1, address.getFlags()); + assertEquals(2, address.getScope()); + assertTrue(address.isIpv6()); + + address = new LinkAddress("[2001:db8::123]/64", 3 /* flags */, 4 /* scope */); + assertEquals(InetAddresses.parseNumericAddress("2001:db8::123"), address.getAddress()); + assertEquals(64, address.getPrefixLength()); + assertEquals(3, address.getFlags()); + assertEquals(4, address.getScope()); + assertTrue(address.isIpv6()); + + // InterfaceAddress doesn't have a constructor. Fetch some from an interface. + List addrs = NetworkInterface.getByName("lo").getInterfaceAddresses(); + + // We expect to find 127.0.0.1/8 and ::1/128, in any order. + LinkAddress ipv4Loopback, ipv6Loopback; + assertEquals(2, addrs.size()); + if (addrs.get(0).getAddress() instanceof Inet4Address) { + ipv4Loopback = new LinkAddress(addrs.get(0)); + ipv6Loopback = new LinkAddress(addrs.get(1)); + } else { + ipv4Loopback = new LinkAddress(addrs.get(1)); + ipv6Loopback = new LinkAddress(addrs.get(0)); + } + + assertEquals(InetAddresses.parseNumericAddress("127.0.0.1"), ipv4Loopback.getAddress()); + assertEquals(8, ipv4Loopback.getPrefixLength()); + + assertEquals(InetAddresses.parseNumericAddress("::1"), ipv6Loopback.getAddress()); + assertEquals(128, ipv6Loopback.getPrefixLength()); + + // Null addresses are rejected. + try { + address = new LinkAddress(null, 24); + fail("Null InetAddress should cause IllegalArgumentException"); + } catch(IllegalArgumentException expected) {} + + try { + address = new LinkAddress((String) null, IFA_F_PERMANENT, RT_SCOPE_UNIVERSE); + fail("Null string should cause IllegalArgumentException"); + } catch(IllegalArgumentException expected) {} + + try { + address = new LinkAddress((InterfaceAddress) null); + fail("Null string should cause NullPointerException"); + } catch(NullPointerException expected) {} + + // Invalid prefix lengths are rejected. + try { + address = new LinkAddress(V4_ADDRESS, -1); + fail("Negative IPv4 prefix length should cause IllegalArgumentException"); + } catch(IllegalArgumentException expected) {} + + try { + address = new LinkAddress(V6_ADDRESS, -1); + fail("Negative IPv6 prefix length should cause IllegalArgumentException"); + } catch(IllegalArgumentException expected) {} + + try { + address = new LinkAddress(V4_ADDRESS, 33); + fail("/33 IPv4 prefix length should cause IllegalArgumentException"); + } catch(IllegalArgumentException expected) {} + + try { + address = new LinkAddress(V4 + "/33", IFA_F_PERMANENT, RT_SCOPE_UNIVERSE); + fail("/33 IPv4 prefix length should cause IllegalArgumentException"); + } catch(IllegalArgumentException expected) {} + + + try { + address = new LinkAddress(V6_ADDRESS, 129, IFA_F_PERMANENT, RT_SCOPE_UNIVERSE); + fail("/129 IPv6 prefix length should cause IllegalArgumentException"); + } catch(IllegalArgumentException expected) {} + + try { + address = new LinkAddress(V6 + "/129", IFA_F_PERMANENT, RT_SCOPE_UNIVERSE); + fail("/129 IPv6 prefix length should cause IllegalArgumentException"); + } catch(IllegalArgumentException expected) {} + + // Multicast addresses are rejected. + try { + address = new LinkAddress("224.0.0.2/32"); + fail("IPv4 multicast address should cause IllegalArgumentException"); + } catch(IllegalArgumentException expected) {} + + try { + address = new LinkAddress("ff02::1/128"); + fail("IPv6 multicast address should cause IllegalArgumentException"); + } catch(IllegalArgumentException expected) {} + } + + @Test + public void testAddressScopes() { + assertEquals(RT_SCOPE_HOST, new LinkAddress("::/128").getScope()); + assertEquals(RT_SCOPE_HOST, new LinkAddress("0.0.0.0/32").getScope()); + + assertEquals(RT_SCOPE_LINK, new LinkAddress("::1/128").getScope()); + assertEquals(RT_SCOPE_LINK, new LinkAddress("127.0.0.5/8").getScope()); + assertEquals(RT_SCOPE_LINK, new LinkAddress("fe80::ace:d00d/64").getScope()); + assertEquals(RT_SCOPE_LINK, new LinkAddress("169.254.5.12/16").getScope()); + + assertEquals(RT_SCOPE_SITE, new LinkAddress("fec0::dead/64").getScope()); + + assertEquals(RT_SCOPE_UNIVERSE, new LinkAddress("10.1.2.3/21").getScope()); + assertEquals(RT_SCOPE_UNIVERSE, new LinkAddress("192.0.2.1/25").getScope()); + assertEquals(RT_SCOPE_UNIVERSE, new LinkAddress("2001:db8::/64").getScope()); + assertEquals(RT_SCOPE_UNIVERSE, new LinkAddress("5000::/127").getScope()); + } + + private void assertIsSameAddressAs(LinkAddress l1, LinkAddress l2) { + assertTrue(l1 + " unexpectedly does not have same address as " + l2, + l1.isSameAddressAs(l2)); + assertTrue(l2 + " unexpectedly does not have same address as " + l1, + l2.isSameAddressAs(l1)); + } + + private void assertIsNotSameAddressAs(LinkAddress l1, LinkAddress l2) { + assertFalse(l1 + " unexpectedly has same address as " + l2, + l1.isSameAddressAs(l2)); + assertFalse(l2 + " unexpectedly has same address as " + l1, + l1.isSameAddressAs(l2)); + } + + @Test + public void testEqualsAndSameAddressAs() { + LinkAddress l1, l2, l3; + + l1 = new LinkAddress("2001:db8::1/64"); + l2 = new LinkAddress("2001:db8::1/64"); + assertEqualBothWays(l1, l2); + assertIsSameAddressAs(l1, l2); + + l2 = new LinkAddress("2001:db8::1/65"); + assertNotEqualEitherWay(l1, l2); + assertIsNotSameAddressAs(l1, l2); + + l2 = new LinkAddress("2001:db8::2/64"); + assertNotEqualEitherWay(l1, l2); + assertIsNotSameAddressAs(l1, l2); + + + l1 = new LinkAddress("192.0.2.1/24"); + l2 = new LinkAddress("192.0.2.1/24"); + assertEqualBothWays(l1, l2); + assertIsSameAddressAs(l1, l2); + + l2 = new LinkAddress("192.0.2.1/23"); + assertNotEqualEitherWay(l1, l2); + assertIsNotSameAddressAs(l1, l2); + + l2 = new LinkAddress("192.0.2.2/24"); + assertNotEqualEitherWay(l1, l2); + assertIsNotSameAddressAs(l1, l2); + + + // Check equals() and isSameAddressAs() on identical addresses with different flags. + l1 = new LinkAddress(V6_ADDRESS, 64); + l2 = new LinkAddress(V6_ADDRESS, 64, 0, RT_SCOPE_UNIVERSE); + assertEqualBothWays(l1, l2); + assertIsSameAddressAs(l1, l2); + + l2 = new LinkAddress(V6_ADDRESS, 64, IFA_F_DEPRECATED, RT_SCOPE_UNIVERSE); + assertNotEqualEitherWay(l1, l2); + assertIsSameAddressAs(l1, l2); + + // Check equals() and isSameAddressAs() on identical addresses with different scope. + l1 = new LinkAddress(V4_ADDRESS, 24); + l2 = new LinkAddress(V4_ADDRESS, 24, 0, RT_SCOPE_UNIVERSE); + assertEqualBothWays(l1, l2); + assertIsSameAddressAs(l1, l2); + + l2 = new LinkAddress(V4_ADDRESS, 24, 0, RT_SCOPE_HOST); + assertNotEqualEitherWay(l1, l2); + assertIsSameAddressAs(l1, l2); + + // Addresses with the same start or end bytes aren't equal between families. + l1 = new LinkAddress("32.1.13.184/24"); + l2 = new LinkAddress("2001:db8::1/24"); + l3 = new LinkAddress("::2001:db8/24"); + + byte[] ipv4Bytes = l1.getAddress().getAddress(); + byte[] l2FirstIPv6Bytes = Arrays.copyOf(l2.getAddress().getAddress(), 4); + byte[] l3LastIPv6Bytes = Arrays.copyOfRange(l3.getAddress().getAddress(), 12, 16); + assertTrue(Arrays.equals(ipv4Bytes, l2FirstIPv6Bytes)); + assertTrue(Arrays.equals(ipv4Bytes, l3LastIPv6Bytes)); + + assertNotEqualEitherWay(l1, l2); + assertIsNotSameAddressAs(l1, l2); + + assertNotEqualEitherWay(l1, l3); + assertIsNotSameAddressAs(l1, l3); + + // Because we use InetAddress, an IPv4 address is equal to its IPv4-mapped address. + // TODO: Investigate fixing this. + String addressString = V4 + "/24"; + l1 = new LinkAddress(addressString); + l2 = new LinkAddress("::ffff:" + addressString); + assertEqualBothWays(l1, l2); + assertIsSameAddressAs(l1, l2); + } + + @Test + public void testHashCode() { + LinkAddress l1, l2; + + l1 = new LinkAddress(V4_ADDRESS, 23); + l2 = new LinkAddress(V4_ADDRESS, 23, 0, RT_SCOPE_HOST); + assertNotEquals(l1.hashCode(), l2.hashCode()); + + l1 = new LinkAddress(V6_ADDRESS, 128); + l2 = new LinkAddress(V6_ADDRESS, 128, IFA_F_TENTATIVE, RT_SCOPE_UNIVERSE); + assertNotEquals(l1.hashCode(), l2.hashCode()); + } + + @Test + public void testParceling() { + LinkAddress l; + + l = new LinkAddress(V6_ADDRESS, 64, 123, 456); + assertParcelingIsLossless(l); + + l = new LinkAddress(V4 + "/28", IFA_F_PERMANENT, RT_SCOPE_LINK); + assertParcelingIsLossless(l); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testLifetimeParceling() { + final LinkAddress l = new LinkAddress(V6_ADDRESS, 64, 123, 456, 1L, 3600000L); + assertParcelingIsLossless(l); + } + + @Test @IgnoreAfter(Build.VERSION_CODES.Q) + public void testFieldCount_Q() { + assertFieldCountEquals(4, LinkAddress.class); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testFieldCount() { + // Make sure any new field is covered by the above parceling tests when changing this number + assertFieldCountEquals(6, LinkAddress.class); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testDeprecationTime() { + try { + new LinkAddress(V6_ADDRESS, 64, 0, 456, + LinkAddress.LIFETIME_UNKNOWN, 100000L); + fail("Only one time provided should cause exception"); + } catch (IllegalArgumentException expected) { } + + try { + new LinkAddress(V6_ADDRESS, 64, 0, 456, + 200000L, 100000L); + fail("deprecation time later than expiration time should cause exception"); + } catch (IllegalArgumentException expected) { } + + try { + new LinkAddress(V6_ADDRESS, 64, 0, 456, + -2, 100000L); + fail("negative deprecation time should cause exception"); + } catch (IllegalArgumentException expected) { } + + LinkAddress addr = new LinkAddress(V6_ADDRESS, 64, 0, 456, 100000L, 200000L); + assertEquals(100000L, addr.getDeprecationTime()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testExpirationTime() { + try { + new LinkAddress(V6_ADDRESS, 64, 0, 456, + 200000L, LinkAddress.LIFETIME_UNKNOWN); + fail("Only one time provided should cause exception"); + } catch (IllegalArgumentException expected) { } + + try { + new LinkAddress(V6_ADDRESS, 64, 0, 456, + 100000L, -2); + fail("negative expiration time should cause exception"); + } catch (IllegalArgumentException expected) { } + + LinkAddress addr = new LinkAddress(V6_ADDRESS, 64, 0, 456, 100000L, 200000L); + assertEquals(200000L, addr.getExpirationTime()); + } + + @Test + public void testGetFlags() { + LinkAddress l = new LinkAddress(V6_ADDRESS, 64, 123, RT_SCOPE_HOST); + assertEquals(123, l.getFlags()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testGetFlags_Deprecation() { + // Test if deprecated bit was added/remove automatically based on the provided deprecation + // time + LinkAddress l = new LinkAddress(V6_ADDRESS, 64, 0, RT_SCOPE_HOST, + 1L, LinkAddress.LIFETIME_PERMANENT); + // Check if the flag is added automatically. + assertTrue((l.getFlags() & IFA_F_DEPRECATED) != 0); + + l = new LinkAddress(V6_ADDRESS, 64, IFA_F_DEPRECATED, RT_SCOPE_HOST, + SystemClock.elapsedRealtime() + 100000L, LinkAddress.LIFETIME_PERMANENT); + // Check if the flag is removed automatically. + assertTrue((l.getFlags() & IFA_F_DEPRECATED) == 0); + + l = new LinkAddress(V6_ADDRESS, 64, IFA_F_DEPRECATED, RT_SCOPE_HOST, + LinkAddress.LIFETIME_PERMANENT, LinkAddress.LIFETIME_PERMANENT); + // Check if the permanent flag is added. + assertTrue((l.getFlags() & IFA_F_PERMANENT) != 0); + + l = new LinkAddress(V6_ADDRESS, 64, IFA_F_PERMANENT, RT_SCOPE_HOST, + 1000L, SystemClock.elapsedRealtime() + 100000L); + // Check if the permanent flag is removed + assertTrue((l.getFlags() & IFA_F_PERMANENT) == 0); + } + + private void assertGlobalPreferred(LinkAddress l, String msg) { + assertTrue(msg, l.isGlobalPreferred()); + } + + private void assertNotGlobalPreferred(LinkAddress l, String msg) { + assertFalse(msg, l.isGlobalPreferred()); + } + + @Test + public void testIsGlobalPreferred() { + LinkAddress l; + + l = new LinkAddress(V4_ADDRESS, 32, 0, RT_SCOPE_UNIVERSE); + assertGlobalPreferred(l, "v4,global,noflags"); + + l = new LinkAddress("10.10.1.7/23", 0, RT_SCOPE_UNIVERSE); + assertGlobalPreferred(l, "v4-rfc1918,global,noflags"); + + l = new LinkAddress("10.10.1.7/23", 0, RT_SCOPE_SITE); + assertNotGlobalPreferred(l, "v4-rfc1918,site-local,noflags"); + + l = new LinkAddress("127.0.0.7/8", 0, RT_SCOPE_HOST); + assertNotGlobalPreferred(l, "v4-localhost,node-local,noflags"); + + l = new LinkAddress(V6_ADDRESS, 64, 0, RT_SCOPE_UNIVERSE); + assertGlobalPreferred(l, "v6,global,noflags"); + + l = new LinkAddress(V6_ADDRESS, 64, IFA_F_PERMANENT, RT_SCOPE_UNIVERSE); + assertGlobalPreferred(l, "v6,global,permanent"); + + // IPv6 ULAs are not acceptable "global preferred" addresses. + l = new LinkAddress("fc12::1/64", 0, RT_SCOPE_UNIVERSE); + assertNotGlobalPreferred(l, "v6,ula1,noflags"); + + l = new LinkAddress("fd34::1/64", 0, RT_SCOPE_UNIVERSE); + assertNotGlobalPreferred(l, "v6,ula2,noflags"); + + l = new LinkAddress(V6_ADDRESS, 64, IFA_F_TEMPORARY, RT_SCOPE_UNIVERSE); + assertGlobalPreferred(l, "v6,global,tempaddr"); + + l = new LinkAddress(V6_ADDRESS, 64, (IFA_F_TEMPORARY|IFA_F_DADFAILED), + RT_SCOPE_UNIVERSE); + assertNotGlobalPreferred(l, "v6,global,tempaddr+dadfailed"); + + l = new LinkAddress(V6_ADDRESS, 64, (IFA_F_TEMPORARY|IFA_F_DEPRECATED), + RT_SCOPE_UNIVERSE); + assertNotGlobalPreferred(l, "v6,global,tempaddr+deprecated"); + + l = new LinkAddress(V6_ADDRESS, 64, IFA_F_TEMPORARY, RT_SCOPE_SITE); + assertNotGlobalPreferred(l, "v6,site-local,tempaddr"); + + l = new LinkAddress(V6_ADDRESS, 64, IFA_F_TEMPORARY, RT_SCOPE_LINK); + assertNotGlobalPreferred(l, "v6,link-local,tempaddr"); + + l = new LinkAddress(V6_ADDRESS, 64, IFA_F_TEMPORARY, RT_SCOPE_HOST); + assertNotGlobalPreferred(l, "v6,node-local,tempaddr"); + + l = new LinkAddress("::1/128", IFA_F_PERMANENT, RT_SCOPE_HOST); + assertNotGlobalPreferred(l, "v6-localhost,node-local,permanent"); + + l = new LinkAddress(V6_ADDRESS, 64, (IFA_F_TEMPORARY|IFA_F_TENTATIVE), + RT_SCOPE_UNIVERSE); + assertNotGlobalPreferred(l, "v6,global,tempaddr+tentative"); + + l = new LinkAddress(V6_ADDRESS, 64, + (IFA_F_TEMPORARY|IFA_F_TENTATIVE|IFA_F_OPTIMISTIC), + RT_SCOPE_UNIVERSE); + assertGlobalPreferred(l, "v6,global,tempaddr+optimistic"); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testIsGlobalPreferred_DeprecatedInFuture() { + final LinkAddress l = new LinkAddress(V6_ADDRESS, 64, IFA_F_DEPRECATED, + RT_SCOPE_UNIVERSE, SystemClock.elapsedRealtime() + 100000, + SystemClock.elapsedRealtime() + 200000); + // Although the deprecated bit is set, but the deprecation time is in the future, test + // if the flag is removed automatically. + assertGlobalPreferred(l, "v6,global,tempaddr+deprecated in the future"); + } +} diff --git a/tests/common/java/android/net/LinkPropertiesTest.java b/tests/common/java/android/net/LinkPropertiesTest.java new file mode 100644 index 0000000000..550953d061 --- /dev/null +++ b/tests/common/java/android/net/LinkPropertiesTest.java @@ -0,0 +1,1271 @@ +/* + * Copyright (C) 2010 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; + +import static android.net.RouteInfo.RTN_THROW; +import static android.net.RouteInfo.RTN_UNICAST; +import static android.net.RouteInfo.RTN_UNREACHABLE; + +import static com.android.testutils.ParcelUtils.assertParcelSane; +import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; +import static com.android.testutils.ParcelUtils.parcelingRoundTrip; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.net.LinkProperties.ProvisioningChange; +import android.os.Build; +import android.system.OsConstants; +import android.util.ArraySet; + +import androidx.core.os.BuildCompat; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.net.module.util.LinkPropertiesUtils.CompareResult; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class LinkPropertiesTest { + @Rule + public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(); + + private static final InetAddress ADDRV4 = address("75.208.6.1"); + private static final InetAddress ADDRV6 = address("2001:0db8:85a3:0000:0000:8a2e:0370:7334"); + private static final InetAddress DNS1 = address("75.208.7.1"); + private static final InetAddress DNS2 = address("69.78.7.1"); + private static final InetAddress DNS6 = address("2001:4860:4860::8888"); + private static final InetAddress PRIVDNS1 = address("1.1.1.1"); + private static final InetAddress PRIVDNS2 = address("1.0.0.1"); + private static final InetAddress PRIVDNS6 = address("2606:4700:4700::1111"); + private static final InetAddress PCSCFV4 = address("10.77.25.37"); + private static final InetAddress PCSCFV6 = address("2001:0db8:85a3:0000:0000:8a2e:0370:1"); + private static final InetAddress GATEWAY1 = address("75.208.8.1"); + private static final InetAddress GATEWAY2 = address("69.78.8.1"); + private static final InetAddress GATEWAY61 = address("fe80::6:0000:613"); + private static final InetAddress GATEWAY62 = address("fe80::6:22%lo"); + private static final InetAddress TESTIPV4ADDR = address("192.168.47.42"); + private static final InetAddress TESTIPV6ADDR = address("fe80::7:33%43"); + private static final Inet4Address DHCPSERVER = (Inet4Address) address("192.0.2.1"); + private static final String NAME = "qmi0"; + private static final String DOMAINS = "google.com"; + private static final String PRIV_DNS_SERVER_NAME = "private.dns.com"; + private static final String TCP_BUFFER_SIZES = "524288,1048576,2097152,262144,524288,1048576"; + private static final int MTU = 1500; + private static final LinkAddress LINKADDRV4 = new LinkAddress(ADDRV4, 32); + private static final LinkAddress LINKADDRV6 = new LinkAddress(ADDRV6, 128); + private static final LinkAddress LINKADDRV6LINKLOCAL = new LinkAddress("fe80::1/64"); + private static final Uri CAPPORT_API_URL = Uri.parse("https://test.example.com/capportapi"); + + // CaptivePortalData cannot be in a constant as it does not exist on Q. + // The test runner also crashes when scanning for tests if it is a return type. + private static Object getCaptivePortalData() { + return new CaptivePortalData.Builder() + .setVenueInfoUrl(Uri.parse("https://test.example.com/venue")).build(); + } + + private static InetAddress address(String addrString) { + return InetAddresses.parseNumericAddress(addrString); + } + + private static boolean isAtLeastR() { + // BuildCompat.isAtLeastR is documented to return false on release SDKs (including R) + return Build.VERSION.SDK_INT > Build.VERSION_CODES.Q || BuildCompat.isAtLeastR(); + } + + private void checkEmpty(final LinkProperties lp) { + assertEquals(0, lp.getAllInterfaceNames().size()); + assertEquals(0, lp.getAllAddresses().size()); + assertEquals(0, lp.getDnsServers().size()); + assertEquals(0, lp.getValidatedPrivateDnsServers().size()); + assertEquals(0, lp.getPcscfServers().size()); + assertEquals(0, lp.getAllRoutes().size()); + assertEquals(0, lp.getAllLinkAddresses().size()); + assertEquals(0, lp.getStackedLinks().size()); + assertEquals(0, lp.getMtu()); + assertNull(lp.getPrivateDnsServerName()); + assertNull(lp.getDomains()); + assertNull(lp.getHttpProxy()); + assertNull(lp.getTcpBufferSizes()); + assertNull(lp.getNat64Prefix()); + assertFalse(lp.isProvisioned()); + assertFalse(lp.isIpv4Provisioned()); + assertFalse(lp.isIpv6Provisioned()); + assertFalse(lp.isPrivateDnsActive()); + + if (isAtLeastR()) { + assertNull(lp.getDhcpServerAddress()); + assertFalse(lp.isWakeOnLanSupported()); + assertNull(lp.getCaptivePortalApiUrl()); + assertNull(lp.getCaptivePortalData()); + } + } + + private LinkProperties makeTestObject() { + final LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(NAME); + lp.addLinkAddress(LINKADDRV4); + lp.addLinkAddress(LINKADDRV6); + lp.addDnsServer(DNS1); + lp.addDnsServer(DNS2); + lp.addValidatedPrivateDnsServer(PRIVDNS1); + lp.addValidatedPrivateDnsServer(PRIVDNS2); + lp.setUsePrivateDns(true); + lp.setPrivateDnsServerName(PRIV_DNS_SERVER_NAME); + lp.addPcscfServer(PCSCFV6); + lp.setDomains(DOMAINS); + lp.addRoute(new RouteInfo(GATEWAY1)); + lp.addRoute(new RouteInfo(GATEWAY2)); + lp.setHttpProxy(ProxyInfo.buildDirectProxy("test", 8888)); + lp.setMtu(MTU); + lp.setTcpBufferSizes(TCP_BUFFER_SIZES); + lp.setNat64Prefix(new IpPrefix("2001:db8:0:64::/96")); + if (isAtLeastR()) { + lp.setDhcpServerAddress(DHCPSERVER); + lp.setWakeOnLanSupported(true); + lp.setCaptivePortalApiUrl(CAPPORT_API_URL); + lp.setCaptivePortalData((CaptivePortalData) getCaptivePortalData()); + } + return lp; + } + + public void assertLinkPropertiesEqual(LinkProperties source, LinkProperties target) { + // Check implementation of equals(), element by element. + assertTrue(source.isIdenticalInterfaceName(target)); + assertTrue(target.isIdenticalInterfaceName(source)); + + assertTrue(source.isIdenticalAddresses(target)); + assertTrue(target.isIdenticalAddresses(source)); + + assertTrue(source.isIdenticalDnses(target)); + assertTrue(target.isIdenticalDnses(source)); + + assertTrue(source.isIdenticalPrivateDns(target)); + assertTrue(target.isIdenticalPrivateDns(source)); + + assertTrue(source.isIdenticalValidatedPrivateDnses(target)); + assertTrue(target.isIdenticalValidatedPrivateDnses(source)); + + assertTrue(source.isIdenticalPcscfs(target)); + assertTrue(target.isIdenticalPcscfs(source)); + + assertTrue(source.isIdenticalRoutes(target)); + assertTrue(target.isIdenticalRoutes(source)); + + assertTrue(source.isIdenticalHttpProxy(target)); + assertTrue(target.isIdenticalHttpProxy(source)); + + assertTrue(source.isIdenticalStackedLinks(target)); + assertTrue(target.isIdenticalStackedLinks(source)); + + assertTrue(source.isIdenticalMtu(target)); + assertTrue(target.isIdenticalMtu(source)); + + assertTrue(source.isIdenticalTcpBufferSizes(target)); + assertTrue(target.isIdenticalTcpBufferSizes(source)); + + if (isAtLeastR()) { + assertTrue(source.isIdenticalDhcpServerAddress(target)); + assertTrue(source.isIdenticalDhcpServerAddress(source)); + + assertTrue(source.isIdenticalWakeOnLan(target)); + assertTrue(target.isIdenticalWakeOnLan(source)); + + assertTrue(source.isIdenticalCaptivePortalApiUrl(target)); + assertTrue(target.isIdenticalCaptivePortalApiUrl(source)); + + assertTrue(source.isIdenticalCaptivePortalData(target)); + assertTrue(target.isIdenticalCaptivePortalData(source)); + } + + // Check result of equals(). + assertTrue(source.equals(target)); + assertTrue(target.equals(source)); + + // Check hashCode. + assertEquals(source.hashCode(), target.hashCode()); + } + + @Test + public void testEqualsNull() { + LinkProperties source = new LinkProperties(); + LinkProperties target = new LinkProperties(); + + assertFalse(source == target); + assertLinkPropertiesEqual(source, target); + } + + @Test + public void testEqualsSameOrder() throws Exception { + LinkProperties source = new LinkProperties(); + source.setInterfaceName(NAME); + // set 2 link addresses + source.addLinkAddress(LINKADDRV4); + source.addLinkAddress(LINKADDRV6); + // set 2 dnses + source.addDnsServer(DNS1); + source.addDnsServer(DNS2); + // set 1 pcscf + source.addPcscfServer(PCSCFV6); + // set 2 gateways + source.addRoute(new RouteInfo(GATEWAY1)); + source.addRoute(new RouteInfo(GATEWAY2)); + source.setMtu(MTU); + + LinkProperties target = new LinkProperties(); + + // All fields are same + target.setInterfaceName(NAME); + target.addLinkAddress(LINKADDRV4); + target.addLinkAddress(LINKADDRV6); + target.addDnsServer(DNS1); + target.addDnsServer(DNS2); + target.addPcscfServer(PCSCFV6); + target.addRoute(new RouteInfo(GATEWAY1)); + target.addRoute(new RouteInfo(GATEWAY2)); + target.setMtu(MTU); + + assertLinkPropertiesEqual(source, target); + + target.clear(); + // change Interface Name + target.setInterfaceName("qmi1"); + target.addLinkAddress(LINKADDRV4); + target.addLinkAddress(LINKADDRV6); + target.addDnsServer(DNS1); + target.addDnsServer(DNS2); + target.addPcscfServer(PCSCFV6); + target.addRoute(new RouteInfo(GATEWAY1)); + target.addRoute(new RouteInfo(GATEWAY2)); + target.setMtu(MTU); + assertFalse(source.equals(target)); + + target.clear(); + target.setInterfaceName(NAME); + // change link addresses + target.addLinkAddress(new LinkAddress(address("75.208.6.2"), 32)); + target.addLinkAddress(LINKADDRV6); + target.addDnsServer(DNS1); + target.addDnsServer(DNS2); + target.addPcscfServer(PCSCFV6); + target.addRoute(new RouteInfo(GATEWAY1)); + target.addRoute(new RouteInfo(GATEWAY2)); + target.setMtu(MTU); + assertFalse(source.equals(target)); + + target.clear(); + target.setInterfaceName(NAME); + target.addLinkAddress(LINKADDRV4); + target.addLinkAddress(LINKADDRV6); + // change dnses + target.addDnsServer(address("75.208.7.2")); + target.addDnsServer(DNS2); + target.addPcscfServer(PCSCFV6); + target.addRoute(new RouteInfo(GATEWAY1)); + target.addRoute(new RouteInfo(GATEWAY2)); + target.setMtu(MTU); + assertFalse(source.equals(target)); + + target.clear(); + target.setInterfaceName(NAME); + target.addLinkAddress(LINKADDRV4); + target.addLinkAddress(LINKADDRV6); + target.addDnsServer(address("75.208.7.2")); + target.addDnsServer(DNS2); + // change pcscf + target.addPcscfServer(address("2001::1")); + target.addRoute(new RouteInfo(GATEWAY1)); + target.addRoute(new RouteInfo(GATEWAY2)); + target.setMtu(MTU); + assertFalse(source.equals(target)); + + target.clear(); + target.setInterfaceName(NAME); + target.addLinkAddress(LINKADDRV4); + target.addLinkAddress(LINKADDRV6); + target.addDnsServer(DNS1); + target.addDnsServer(DNS2); + // change gateway + target.addRoute(new RouteInfo(address("75.208.8.2"))); + target.setMtu(MTU); + target.addRoute(new RouteInfo(GATEWAY2)); + assertFalse(source.equals(target)); + + target.clear(); + target.setInterfaceName(NAME); + target.addLinkAddress(LINKADDRV4); + target.addLinkAddress(LINKADDRV6); + target.addDnsServer(DNS1); + target.addDnsServer(DNS2); + target.addRoute(new RouteInfo(GATEWAY1)); + target.addRoute(new RouteInfo(GATEWAY2)); + // change mtu + target.setMtu(1440); + assertFalse(source.equals(target)); + } + + @Test + public void testEqualsDifferentOrder() throws Exception { + LinkProperties source = new LinkProperties(); + source.setInterfaceName(NAME); + // set 2 link addresses + source.addLinkAddress(LINKADDRV4); + source.addLinkAddress(LINKADDRV6); + // set 2 dnses + source.addDnsServer(DNS1); + source.addDnsServer(DNS2); + // set 2 gateways + source.addRoute(new RouteInfo(LINKADDRV4, GATEWAY1)); + source.addRoute(new RouteInfo(GATEWAY2)); + source.setMtu(MTU); + + LinkProperties target = new LinkProperties(); + // Exchange order + target.setInterfaceName(NAME); + target.addLinkAddress(LINKADDRV6); + target.addLinkAddress(LINKADDRV4); + target.addDnsServer(DNS2); + target.addDnsServer(DNS1); + target.addRoute(new RouteInfo(GATEWAY2)); + target.addRoute(new RouteInfo(LINKADDRV4, GATEWAY1)); + target.setMtu(MTU); + + assertLinkPropertiesEqual(source, target); + } + + @Test + public void testEqualsDuplicated() throws Exception { + LinkProperties source = new LinkProperties(); + // set 3 link addresses, eg, [A, A, B] + source.addLinkAddress(LINKADDRV4); + source.addLinkAddress(LINKADDRV4); + source.addLinkAddress(LINKADDRV6); + + LinkProperties target = new LinkProperties(); + // set 3 link addresses, eg, [A, B, B] + target.addLinkAddress(LINKADDRV4); + target.addLinkAddress(LINKADDRV6); + target.addLinkAddress(LINKADDRV6); + + assertLinkPropertiesEqual(source, target); + } + + private void assertAllRoutesHaveInterface(String iface, LinkProperties lp) { + for (RouteInfo r : lp.getRoutes()) { + assertEquals(iface, r.getInterface()); + } + } + + private void assertAllRoutesNotHaveInterface(String iface, LinkProperties lp) { + for (RouteInfo r : lp.getRoutes()) { + assertNotEquals(iface, r.getInterface()); + } + } + + @Test + public void testRouteInterfaces() { + LinkAddress prefix1 = new LinkAddress(address("2001:db8:1::"), 48); + LinkAddress prefix2 = new LinkAddress(address("2001:db8:2::"), 48); + InetAddress address = ADDRV6; + + // Add a route with no interface to a LinkProperties with no interface. No errors. + LinkProperties lp = new LinkProperties(); + RouteInfo r = new RouteInfo(prefix1, address, null); + assertTrue(lp.addRoute(r)); + assertEquals(1, lp.getRoutes().size()); + assertAllRoutesHaveInterface(null, lp); + + // Adding the same route twice has no effect. + assertFalse(lp.addRoute(r)); + assertEquals(1, lp.getRoutes().size()); + + // Add a route with an interface. Expect an exception. + r = new RouteInfo(prefix2, address, "wlan0"); + try { + lp.addRoute(r); + fail("Adding wlan0 route to LP with no interface, expect exception"); + } catch (IllegalArgumentException expected) {} + + // Change the interface name. All the routes should change their interface name too. + lp.setInterfaceName("rmnet0"); + assertAllRoutesHaveInterface("rmnet0", lp); + assertAllRoutesNotHaveInterface(null, lp); + assertAllRoutesNotHaveInterface("wlan0", lp); + + // Now add a route with the wrong interface. This causes an exception too. + try { + lp.addRoute(r); + fail("Adding wlan0 route to rmnet0 LP, expect exception"); + } catch (IllegalArgumentException expected) {} + + // If the interface name matches, the route is added. + r = new RouteInfo(prefix2, null, "wlan0"); + lp.setInterfaceName("wlan0"); + lp.addRoute(r); + assertEquals(2, lp.getRoutes().size()); + assertAllRoutesHaveInterface("wlan0", lp); + assertAllRoutesNotHaveInterface("rmnet0", lp); + + // Routes with null interfaces are converted to wlan0. + r = RouteInfo.makeHostRoute(ADDRV6, null); + lp.addRoute(r); + assertEquals(3, lp.getRoutes().size()); + assertAllRoutesHaveInterface("wlan0", lp); + + // Check routes are updated correctly when calling setInterfaceName. + LinkProperties lp2 = new LinkProperties(lp); + assertAllRoutesHaveInterface("wlan0", lp2); + final CompareResult cr1 = + new CompareResult<>(lp.getAllRoutes(), lp2.getAllRoutes()); + assertEquals(0, cr1.added.size()); + assertEquals(0, cr1.removed.size()); + + lp2.setInterfaceName("p2p0"); + assertAllRoutesHaveInterface("p2p0", lp2); + assertAllRoutesNotHaveInterface("wlan0", lp2); + final CompareResult cr2 = + new CompareResult<>(lp.getAllRoutes(), lp2.getAllRoutes()); + assertEquals(3, cr2.added.size()); + assertEquals(3, cr2.removed.size()); + + // Remove route with incorrect interface, no route removed. + lp.removeRoute(new RouteInfo(prefix2, null, null)); + assertEquals(3, lp.getRoutes().size()); + + // Check remove works when interface is correct. + lp.removeRoute(new RouteInfo(prefix2, null, "wlan0")); + assertEquals(2, lp.getRoutes().size()); + assertAllRoutesHaveInterface("wlan0", lp); + assertAllRoutesNotHaveInterface("p2p0", lp); + } + + @Test + public void testStackedInterfaces() { + LinkProperties rmnet0 = new LinkProperties(); + rmnet0.setInterfaceName("rmnet0"); + rmnet0.addLinkAddress(LINKADDRV6); + + LinkProperties clat4 = new LinkProperties(); + clat4.setInterfaceName("clat4"); + clat4.addLinkAddress(LINKADDRV4); + + assertEquals(0, rmnet0.getStackedLinks().size()); + assertEquals(1, rmnet0.getAddresses().size()); + assertEquals(1, rmnet0.getLinkAddresses().size()); + assertEquals(1, rmnet0.getAllAddresses().size()); + assertEquals(1, rmnet0.getAllLinkAddresses().size()); + assertEquals(1, rmnet0.getAllInterfaceNames().size()); + assertEquals("rmnet0", rmnet0.getAllInterfaceNames().get(0)); + + rmnet0.addStackedLink(clat4); + assertEquals(1, rmnet0.getStackedLinks().size()); + assertEquals(1, rmnet0.getAddresses().size()); + assertEquals(1, rmnet0.getLinkAddresses().size()); + assertEquals(2, rmnet0.getAllAddresses().size()); + assertEquals(2, rmnet0.getAllLinkAddresses().size()); + assertEquals(2, rmnet0.getAllInterfaceNames().size()); + assertEquals("rmnet0", rmnet0.getAllInterfaceNames().get(0)); + assertEquals("clat4", rmnet0.getAllInterfaceNames().get(1)); + + rmnet0.addStackedLink(clat4); + assertEquals(1, rmnet0.getStackedLinks().size()); + assertEquals(1, rmnet0.getAddresses().size()); + assertEquals(1, rmnet0.getLinkAddresses().size()); + assertEquals(2, rmnet0.getAllAddresses().size()); + assertEquals(2, rmnet0.getAllLinkAddresses().size()); + assertEquals(2, rmnet0.getAllInterfaceNames().size()); + assertEquals("rmnet0", rmnet0.getAllInterfaceNames().get(0)); + assertEquals("clat4", rmnet0.getAllInterfaceNames().get(1)); + + assertEquals(0, clat4.getStackedLinks().size()); + + // Modify an item in the returned collection to see what happens. + for (LinkProperties link : rmnet0.getStackedLinks()) { + if (link.getInterfaceName().equals("clat4")) { + link.setInterfaceName("newname"); + } + } + for (LinkProperties link : rmnet0.getStackedLinks()) { + assertFalse("newname".equals(link.getInterfaceName())); + } + + assertTrue(rmnet0.removeStackedLink("clat4")); + assertEquals(0, rmnet0.getStackedLinks().size()); + assertEquals(1, rmnet0.getAddresses().size()); + assertEquals(1, rmnet0.getLinkAddresses().size()); + assertEquals(1, rmnet0.getAllAddresses().size()); + assertEquals(1, rmnet0.getAllLinkAddresses().size()); + assertEquals(1, rmnet0.getAllInterfaceNames().size()); + assertEquals("rmnet0", rmnet0.getAllInterfaceNames().get(0)); + + assertFalse(rmnet0.removeStackedLink("clat4")); + } + + private LinkAddress getFirstLinkAddress(LinkProperties lp) { + return lp.getLinkAddresses().iterator().next(); + } + + @Test + public void testAddressMethods() { + LinkProperties lp = new LinkProperties(); + + // No addresses. + assertFalse(lp.hasIpv4Address()); + assertFalse(lp.hasGlobalIpv6Address()); + + // Addresses on stacked links don't count. + LinkProperties stacked = new LinkProperties(); + stacked.setInterfaceName("stacked"); + lp.addStackedLink(stacked); + stacked.addLinkAddress(LINKADDRV4); + stacked.addLinkAddress(LINKADDRV6); + assertTrue(stacked.hasIpv4Address()); + assertTrue(stacked.hasGlobalIpv6Address()); + assertFalse(lp.hasIpv4Address()); + assertFalse(lp.hasGlobalIpv6Address()); + lp.removeStackedLink("stacked"); + assertFalse(lp.hasIpv4Address()); + assertFalse(lp.hasGlobalIpv6Address()); + + // Addresses on the base link. + // Check the return values of hasIpvXAddress and ensure the add/remove methods return true + // iff something changes. + assertEquals(0, lp.getLinkAddresses().size()); + assertTrue(lp.addLinkAddress(LINKADDRV6)); + assertEquals(1, lp.getLinkAddresses().size()); + assertFalse(lp.hasIpv4Address()); + assertTrue(lp.hasGlobalIpv6Address()); + + assertTrue(lp.removeLinkAddress(LINKADDRV6)); + assertEquals(0, lp.getLinkAddresses().size()); + + assertTrue(lp.addLinkAddress(LINKADDRV6LINKLOCAL)); + assertEquals(1, lp.getLinkAddresses().size()); + assertFalse(lp.hasGlobalIpv6Address()); + + assertTrue(lp.addLinkAddress(LINKADDRV4)); + assertEquals(2, lp.getLinkAddresses().size()); + assertTrue(lp.hasIpv4Address()); + assertFalse(lp.hasGlobalIpv6Address()); + + assertTrue(lp.addLinkAddress(LINKADDRV6)); + assertEquals(3, lp.getLinkAddresses().size()); + assertTrue(lp.hasIpv4Address()); + assertTrue(lp.hasGlobalIpv6Address()); + + assertTrue(lp.removeLinkAddress(LINKADDRV6LINKLOCAL)); + assertEquals(2, lp.getLinkAddresses().size()); + assertTrue(lp.hasIpv4Address()); + assertTrue(lp.hasGlobalIpv6Address()); + + // Adding an address twice has no effect. + // Removing an address that's not present has no effect. + assertFalse(lp.addLinkAddress(LINKADDRV4)); + assertEquals(2, lp.getLinkAddresses().size()); + assertTrue(lp.hasIpv4Address()); + assertTrue(lp.removeLinkAddress(LINKADDRV4)); + assertEquals(1, lp.getLinkAddresses().size()); + assertFalse(lp.hasIpv4Address()); + assertFalse(lp.removeLinkAddress(LINKADDRV4)); + assertEquals(1, lp.getLinkAddresses().size()); + + // Adding an address that's already present but with different properties causes the + // existing address to be updated and returns true. + // Start with only LINKADDRV6. + assertEquals(1, lp.getLinkAddresses().size()); + assertEquals(LINKADDRV6, getFirstLinkAddress(lp)); + + // Create a LinkAddress object for the same address, but with different flags. + LinkAddress deprecated = new LinkAddress(ADDRV6, 128, + OsConstants.IFA_F_DEPRECATED, OsConstants.RT_SCOPE_UNIVERSE); + assertTrue(deprecated.isSameAddressAs(LINKADDRV6)); + assertFalse(deprecated.equals(LINKADDRV6)); + + // Check that adding it updates the existing address instead of adding a new one. + assertTrue(lp.addLinkAddress(deprecated)); + assertEquals(1, lp.getLinkAddresses().size()); + assertEquals(deprecated, getFirstLinkAddress(lp)); + assertFalse(LINKADDRV6.equals(getFirstLinkAddress(lp))); + + // Removing LINKADDRV6 removes deprecated, because removing addresses ignores properties. + assertTrue(lp.removeLinkAddress(LINKADDRV6)); + assertEquals(0, lp.getLinkAddresses().size()); + } + + @Test + public void testLinkAddresses() { + final LinkProperties lp = new LinkProperties(); + lp.addLinkAddress(LINKADDRV4); + lp.addLinkAddress(LINKADDRV6); + + final LinkProperties lp2 = new LinkProperties(); + lp2.addLinkAddress(LINKADDRV6); + + final LinkProperties lp3 = new LinkProperties(); + final List linkAddresses = Arrays.asList(LINKADDRV4); + lp3.setLinkAddresses(linkAddresses); + + assertFalse(lp.equals(lp2)); + assertFalse(lp2.equals(lp3)); + + lp.removeLinkAddress(LINKADDRV4); + assertTrue(lp.equals(lp2)); + + lp2.setLinkAddresses(lp3.getLinkAddresses()); + assertTrue(lp2.equals(lp3)); + } + + @Test + public void testNat64Prefix() throws Exception { + LinkProperties lp = new LinkProperties(); + lp.addLinkAddress(LINKADDRV4); + lp.addLinkAddress(LINKADDRV6); + + assertNull(lp.getNat64Prefix()); + + IpPrefix p = new IpPrefix("64:ff9b::/96"); + lp.setNat64Prefix(p); + assertEquals(p, lp.getNat64Prefix()); + + p = new IpPrefix("2001:db8:a:b:1:2:3::/96"); + lp.setNat64Prefix(p); + assertEquals(p, lp.getNat64Prefix()); + + p = new IpPrefix("2001:db8:a:b:1:2::/80"); + try { + lp.setNat64Prefix(p); + } catch (IllegalArgumentException expected) { + } + + p = new IpPrefix("64:ff9b::/64"); + try { + lp.setNat64Prefix(p); + } catch (IllegalArgumentException expected) { + } + + assertEquals(new IpPrefix("2001:db8:a:b:1:2:3::/96"), lp.getNat64Prefix()); + + lp.setNat64Prefix(null); + assertNull(lp.getNat64Prefix()); + } + + @Test + public void testIsProvisioned() { + LinkProperties lp4 = new LinkProperties(); + assertFalse("v4only:empty", lp4.isProvisioned()); + lp4.addLinkAddress(LINKADDRV4); + assertFalse("v4only:addr-only", lp4.isProvisioned()); + lp4.addDnsServer(DNS1); + assertFalse("v4only:addr+dns", lp4.isProvisioned()); + lp4.addRoute(new RouteInfo(GATEWAY1)); + assertTrue("v4only:addr+dns+route", lp4.isProvisioned()); + assertTrue("v4only:addr+dns+route", lp4.isIpv4Provisioned()); + assertFalse("v4only:addr+dns+route", lp4.isIpv6Provisioned()); + + LinkProperties lp6 = new LinkProperties(); + assertFalse("v6only:empty", lp6.isProvisioned()); + lp6.addLinkAddress(LINKADDRV6LINKLOCAL); + assertFalse("v6only:fe80-only", lp6.isProvisioned()); + lp6.addDnsServer(DNS6); + assertFalse("v6only:fe80+dns", lp6.isProvisioned()); + lp6.addRoute(new RouteInfo(GATEWAY61)); + assertFalse("v6only:fe80+dns+route", lp6.isProvisioned()); + lp6.addLinkAddress(LINKADDRV6); + assertTrue("v6only:fe80+global+dns+route", lp6.isIpv6Provisioned()); + assertTrue("v6only:fe80+global+dns+route", lp6.isProvisioned()); + lp6.removeLinkAddress(LINKADDRV6LINKLOCAL); + assertFalse("v6only:global+dns+route", lp6.isIpv4Provisioned()); + assertTrue("v6only:global+dns+route", lp6.isIpv6Provisioned()); + assertTrue("v6only:global+dns+route", lp6.isProvisioned()); + + LinkProperties lp46 = new LinkProperties(); + lp46.addLinkAddress(LINKADDRV4); + lp46.addLinkAddress(LINKADDRV6); + lp46.addDnsServer(DNS1); + lp46.addDnsServer(DNS6); + assertFalse("dualstack:missing-routes", lp46.isProvisioned()); + lp46.addRoute(new RouteInfo(GATEWAY1)); + assertTrue("dualstack:v4-provisioned", lp46.isIpv4Provisioned()); + assertFalse("dualstack:v4-provisioned", lp46.isIpv6Provisioned()); + assertTrue("dualstack:v4-provisioned", lp46.isProvisioned()); + lp46.addRoute(new RouteInfo(GATEWAY61)); + assertTrue("dualstack:both-provisioned", lp46.isIpv4Provisioned()); + assertTrue("dualstack:both-provisioned", lp46.isIpv6Provisioned()); + assertTrue("dualstack:both-provisioned", lp46.isProvisioned()); + + // A link with an IPv6 address and default route, but IPv4 DNS server. + LinkProperties mixed = new LinkProperties(); + mixed.addLinkAddress(LINKADDRV6); + mixed.addDnsServer(DNS1); + mixed.addRoute(new RouteInfo(GATEWAY61)); + assertFalse("mixed:addr6+route6+dns4", mixed.isIpv4Provisioned()); + assertFalse("mixed:addr6+route6+dns4", mixed.isIpv6Provisioned()); + assertFalse("mixed:addr6+route6+dns4", mixed.isProvisioned()); + } + + @Test + public void testCompareProvisioning() { + LinkProperties v4lp = new LinkProperties(); + v4lp.addLinkAddress(LINKADDRV4); + v4lp.addRoute(new RouteInfo(GATEWAY1)); + v4lp.addDnsServer(DNS1); + assertTrue(v4lp.isProvisioned()); + + LinkProperties v4r = new LinkProperties(v4lp); + v4r.removeDnsServer(DNS1); + assertFalse(v4r.isProvisioned()); + + assertEquals(ProvisioningChange.STILL_NOT_PROVISIONED, + LinkProperties.compareProvisioning(v4r, v4r)); + assertEquals(ProvisioningChange.LOST_PROVISIONING, + LinkProperties.compareProvisioning(v4lp, v4r)); + assertEquals(ProvisioningChange.GAINED_PROVISIONING, + LinkProperties.compareProvisioning(v4r, v4lp)); + assertEquals(ProvisioningChange.STILL_PROVISIONED, + LinkProperties.compareProvisioning(v4lp, v4lp)); + + // Check that losing IPv4 provisioning on a dualstack network is + // seen as a total loss of provisioning. + LinkProperties v6lp = new LinkProperties(); + v6lp.addLinkAddress(LINKADDRV6); + v6lp.addRoute(new RouteInfo(GATEWAY61)); + v6lp.addDnsServer(DNS6); + assertFalse(v6lp.isIpv4Provisioned()); + assertTrue(v6lp.isIpv6Provisioned()); + assertTrue(v6lp.isProvisioned()); + + LinkProperties v46lp = new LinkProperties(v6lp); + v46lp.addLinkAddress(LINKADDRV4); + v46lp.addRoute(new RouteInfo(GATEWAY1)); + v46lp.addDnsServer(DNS1); + assertTrue(v46lp.isIpv4Provisioned()); + assertTrue(v46lp.isIpv6Provisioned()); + assertTrue(v46lp.isProvisioned()); + + assertEquals(ProvisioningChange.STILL_PROVISIONED, + LinkProperties.compareProvisioning(v4lp, v46lp)); + assertEquals(ProvisioningChange.STILL_PROVISIONED, + LinkProperties.compareProvisioning(v6lp, v46lp)); + assertEquals(ProvisioningChange.LOST_PROVISIONING, + LinkProperties.compareProvisioning(v46lp, v6lp)); + assertEquals(ProvisioningChange.LOST_PROVISIONING, + LinkProperties.compareProvisioning(v46lp, v4lp)); + + // Check that losing and gaining a secondary router does not change + // the provisioning status. + LinkProperties v6lp2 = new LinkProperties(v6lp); + v6lp2.addRoute(new RouteInfo(GATEWAY62)); + assertTrue(v6lp2.isProvisioned()); + + assertEquals(ProvisioningChange.STILL_PROVISIONED, + LinkProperties.compareProvisioning(v6lp2, v6lp)); + assertEquals(ProvisioningChange.STILL_PROVISIONED, + LinkProperties.compareProvisioning(v6lp, v6lp2)); + } + + @Test + public void testIsReachable() { + final LinkProperties v4lp = new LinkProperties(); + assertFalse(v4lp.isReachable(DNS1)); + assertFalse(v4lp.isReachable(DNS2)); + + // Add an on-link route, making the on-link DNS server reachable, + // but there is still no IPv4 address. + assertTrue(v4lp.addRoute(new RouteInfo(new IpPrefix(address("75.208.0.0"), 16)))); + assertFalse(v4lp.isReachable(DNS1)); + assertFalse(v4lp.isReachable(DNS2)); + + // Adding an IPv4 address (right now, any IPv4 address) means we use + // the routes to compute likely reachability. + assertTrue(v4lp.addLinkAddress(new LinkAddress(ADDRV4, 16))); + assertTrue(v4lp.isReachable(DNS1)); + assertFalse(v4lp.isReachable(DNS2)); + + // Adding a default route makes the off-link DNS server reachable. + assertTrue(v4lp.addRoute(new RouteInfo(GATEWAY1))); + assertTrue(v4lp.isReachable(DNS1)); + assertTrue(v4lp.isReachable(DNS2)); + + final LinkProperties v6lp = new LinkProperties(); + final InetAddress kLinkLocalDns = address("fe80::6:1"); + final InetAddress kLinkLocalDnsWithScope = address("fe80::6:2%43"); + final InetAddress kOnLinkDns = address("2001:db8:85a3::53"); + assertFalse(v6lp.isReachable(kLinkLocalDns)); + assertFalse(v6lp.isReachable(kLinkLocalDnsWithScope)); + assertFalse(v6lp.isReachable(kOnLinkDns)); + assertFalse(v6lp.isReachable(DNS6)); + + // Add a link-local route, making the link-local DNS servers reachable. Because + // we assume the presence of an IPv6 link-local address, link-local DNS servers + // are considered reachable, but only those with a non-zero scope identifier. + assertTrue(v6lp.addRoute(new RouteInfo(new IpPrefix(address("fe80::"), 64)))); + assertFalse(v6lp.isReachable(kLinkLocalDns)); + assertTrue(v6lp.isReachable(kLinkLocalDnsWithScope)); + assertFalse(v6lp.isReachable(kOnLinkDns)); + assertFalse(v6lp.isReachable(DNS6)); + + // Add a link-local address--nothing changes. + assertTrue(v6lp.addLinkAddress(LINKADDRV6LINKLOCAL)); + assertFalse(v6lp.isReachable(kLinkLocalDns)); + assertTrue(v6lp.isReachable(kLinkLocalDnsWithScope)); + assertFalse(v6lp.isReachable(kOnLinkDns)); + assertFalse(v6lp.isReachable(DNS6)); + + // Add a global route on link, but no global address yet. DNS servers reachable + // via a route that doesn't require a gateway: give them the benefit of the + // doubt and hope the link-local source address suffices for communication. + assertTrue(v6lp.addRoute(new RouteInfo(new IpPrefix(address("2001:db8:85a3::"), 64)))); + assertFalse(v6lp.isReachable(kLinkLocalDns)); + assertTrue(v6lp.isReachable(kLinkLocalDnsWithScope)); + assertTrue(v6lp.isReachable(kOnLinkDns)); + assertFalse(v6lp.isReachable(DNS6)); + + // Add a global address; the on-link global address DNS server is (still) + // presumed reachable. + assertTrue(v6lp.addLinkAddress(new LinkAddress(ADDRV6, 64))); + assertFalse(v6lp.isReachable(kLinkLocalDns)); + assertTrue(v6lp.isReachable(kLinkLocalDnsWithScope)); + assertTrue(v6lp.isReachable(kOnLinkDns)); + assertFalse(v6lp.isReachable(DNS6)); + + // Adding a default route makes the off-link DNS server reachable. + assertTrue(v6lp.addRoute(new RouteInfo(GATEWAY62))); + assertFalse(v6lp.isReachable(kLinkLocalDns)); + assertTrue(v6lp.isReachable(kLinkLocalDnsWithScope)); + assertTrue(v6lp.isReachable(kOnLinkDns)); + assertTrue(v6lp.isReachable(DNS6)); + + // Check isReachable on stacked links. This requires that the source IP address be assigned + // on the interface returned by the route lookup. + LinkProperties stacked = new LinkProperties(); + + // Can't add a stacked link without an interface name. + stacked.setInterfaceName("v4-test0"); + v6lp.addStackedLink(stacked); + + InetAddress stackedAddress = address("192.0.0.4"); + LinkAddress stackedLinkAddress = new LinkAddress(stackedAddress, 32); + assertFalse(v6lp.isReachable(stackedAddress)); + stacked.addLinkAddress(stackedLinkAddress); + assertFalse(v6lp.isReachable(stackedAddress)); + stacked.addRoute(new RouteInfo(stackedLinkAddress)); + assertTrue(stacked.isReachable(stackedAddress)); + assertTrue(v6lp.isReachable(stackedAddress)); + + assertFalse(v6lp.isReachable(DNS1)); + stacked.addRoute(new RouteInfo((IpPrefix) null, stackedAddress)); + assertTrue(v6lp.isReachable(DNS1)); + } + + @Test + public void testLinkPropertiesEnsureDirectlyConnectedRoutes() { + // IPv4 case: no route added initially + LinkProperties rmnet0 = new LinkProperties(); + rmnet0.setInterfaceName("rmnet0"); + rmnet0.addLinkAddress(new LinkAddress("10.0.0.2/8")); + RouteInfo directRoute0 = new RouteInfo(new IpPrefix("10.0.0.0/8"), null, + rmnet0.getInterfaceName()); + + // Since no routes is added explicitly, getAllRoutes() should return empty. + assertTrue(rmnet0.getAllRoutes().isEmpty()); + rmnet0.ensureDirectlyConnectedRoutes(); + // ensureDirectlyConnectedRoutes() should have added the missing local route. + assertEqualRoutes(Collections.singletonList(directRoute0), rmnet0.getAllRoutes()); + + // IPv4 case: both direct and default routes added initially + LinkProperties rmnet1 = new LinkProperties(); + rmnet1.setInterfaceName("rmnet1"); + rmnet1.addLinkAddress(new LinkAddress("10.0.0.3/8")); + RouteInfo defaultRoute1 = new RouteInfo((IpPrefix) null, address("10.0.0.1"), + rmnet1.getInterfaceName()); + RouteInfo directRoute1 = new RouteInfo(new IpPrefix("10.0.0.0/8"), null, + rmnet1.getInterfaceName()); + rmnet1.addRoute(defaultRoute1); + rmnet1.addRoute(directRoute1); + + // Check added routes + assertEqualRoutes(Arrays.asList(defaultRoute1, directRoute1), rmnet1.getAllRoutes()); + // ensureDirectlyConnectedRoutes() shouldn't change the routes since direct connected + // route is already part of the configuration. + rmnet1.ensureDirectlyConnectedRoutes(); + assertEqualRoutes(Arrays.asList(defaultRoute1, directRoute1), rmnet1.getAllRoutes()); + + // IPv6 case: only default routes added initially + LinkProperties rmnet2 = new LinkProperties(); + rmnet2.setInterfaceName("rmnet2"); + rmnet2.addLinkAddress(new LinkAddress("fe80::cafe/64")); + rmnet2.addLinkAddress(new LinkAddress("2001:db8::2/64")); + RouteInfo defaultRoute2 = new RouteInfo((IpPrefix) null, address("2001:db8::1"), + rmnet2.getInterfaceName()); + RouteInfo directRoute2 = new RouteInfo(new IpPrefix("2001:db8::/64"), null, + rmnet2.getInterfaceName()); + RouteInfo linkLocalRoute2 = new RouteInfo(new IpPrefix("fe80::/64"), null, + rmnet2.getInterfaceName()); + rmnet2.addRoute(defaultRoute2); + + assertEqualRoutes(Arrays.asList(defaultRoute2), rmnet2.getAllRoutes()); + rmnet2.ensureDirectlyConnectedRoutes(); + assertEqualRoutes(Arrays.asList(defaultRoute2, directRoute2, linkLocalRoute2), + rmnet2.getAllRoutes()); + + // Corner case: no interface name + LinkProperties rmnet3 = new LinkProperties(); + rmnet3.addLinkAddress(new LinkAddress("192.168.0.2/24")); + RouteInfo directRoute3 = new RouteInfo(new IpPrefix("192.168.0.0/24"), null, + rmnet3.getInterfaceName()); + + assertTrue(rmnet3.getAllRoutes().isEmpty()); + rmnet3.ensureDirectlyConnectedRoutes(); + assertEqualRoutes(Collections.singletonList(directRoute3), rmnet3.getAllRoutes()); + } + + private void assertEqualRoutes(Collection expected, Collection actual) { + Set expectedSet = new ArraySet<>(expected); + Set actualSet = new ArraySet<>(actual); + // Duplicated entries in actual routes are considered failures + assertEquals(actual.size(), actualSet.size()); + + assertEquals(expectedSet, actualSet); + } + + private static LinkProperties makeLinkPropertiesForParceling() { + LinkProperties source = new LinkProperties(); + source.setInterfaceName(NAME); + + source.addLinkAddress(LINKADDRV4); + source.addLinkAddress(LINKADDRV6); + + source.addDnsServer(DNS1); + source.addDnsServer(DNS2); + source.addDnsServer(GATEWAY62); + + source.addPcscfServer(TESTIPV4ADDR); + source.addPcscfServer(TESTIPV6ADDR); + + source.setUsePrivateDns(true); + source.setPrivateDnsServerName(PRIV_DNS_SERVER_NAME); + + source.setDomains(DOMAINS); + + source.addRoute(new RouteInfo(GATEWAY1)); + source.addRoute(new RouteInfo(GATEWAY2)); + + source.addValidatedPrivateDnsServer(DNS6); + source.addValidatedPrivateDnsServer(GATEWAY61); + source.addValidatedPrivateDnsServer(TESTIPV6ADDR); + + source.setHttpProxy(ProxyInfo.buildDirectProxy("test", 8888)); + + source.setMtu(MTU); + + source.setTcpBufferSizes(TCP_BUFFER_SIZES); + + source.setNat64Prefix(new IpPrefix("2001:db8:1:2:64:64::/96")); + + final LinkProperties stacked = new LinkProperties(); + stacked.setInterfaceName("test-stacked"); + source.addStackedLink(stacked); + + return source; + } + + @Test @IgnoreAfter(Build.VERSION_CODES.Q) + public void testLinkPropertiesParcelable_Q() throws Exception { + final LinkProperties source = makeLinkPropertiesForParceling(); + assertParcelSane(source, 14 /* fieldCount */); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testLinkPropertiesParcelable() throws Exception { + final LinkProperties source = makeLinkPropertiesForParceling(); + + source.setWakeOnLanSupported(true); + source.setCaptivePortalApiUrl(CAPPORT_API_URL); + source.setCaptivePortalData((CaptivePortalData) getCaptivePortalData()); + source.setDhcpServerAddress((Inet4Address) GATEWAY1); + assertParcelSane(new LinkProperties(source, true /* parcelSensitiveFields */), + 18 /* fieldCount */); + + // Verify that without using a sensitiveFieldsParcelingCopy, sensitive fields are cleared. + final LinkProperties sanitized = new LinkProperties(source); + sanitized.setCaptivePortalApiUrl(null); + sanitized.setCaptivePortalData(null); + assertEquals(sanitized, parcelingRoundTrip(source)); + } + + // Parceling of the scope was broken until Q-QPR2 + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testLinkLocalDnsServerParceling() throws Exception { + final String strAddress = "fe80::1%lo"; + final LinkProperties lp = new LinkProperties(); + lp.addDnsServer(address(strAddress)); + final LinkProperties unparceled = parcelingRoundTrip(lp); + // Inet6Address#equals does not test for the scope id + assertEquals(strAddress, unparceled.getDnsServers().get(0).getHostAddress()); + } + + @Test + public void testParcelUninitialized() throws Exception { + LinkProperties empty = new LinkProperties(); + assertParcelingIsLossless(empty); + } + + @Test + public void testConstructor() { + LinkProperties lp = new LinkProperties(); + checkEmpty(lp); + assertLinkPropertiesEqual(lp, new LinkProperties(lp)); + assertLinkPropertiesEqual(lp, new LinkProperties()); + + lp = makeTestObject(); + assertLinkPropertiesEqual(lp, new LinkProperties(lp)); + } + + @Test + public void testDnsServers() { + final LinkProperties lp = new LinkProperties(); + final List dnsServers = Arrays.asList(DNS1, DNS2); + lp.setDnsServers(dnsServers); + assertEquals(2, lp.getDnsServers().size()); + assertEquals(DNS1, lp.getDnsServers().get(0)); + assertEquals(DNS2, lp.getDnsServers().get(1)); + + lp.removeDnsServer(DNS1); + assertEquals(1, lp.getDnsServers().size()); + assertEquals(DNS2, lp.getDnsServers().get(0)); + + lp.addDnsServer(DNS6); + assertEquals(2, lp.getDnsServers().size()); + assertEquals(DNS2, lp.getDnsServers().get(0)); + assertEquals(DNS6, lp.getDnsServers().get(1)); + } + + @Test + public void testValidatedPrivateDnsServers() { + final LinkProperties lp = new LinkProperties(); + final List privDnsServers = Arrays.asList(PRIVDNS1, PRIVDNS2); + lp.setValidatedPrivateDnsServers(privDnsServers); + assertEquals(2, lp.getValidatedPrivateDnsServers().size()); + assertEquals(PRIVDNS1, lp.getValidatedPrivateDnsServers().get(0)); + assertEquals(PRIVDNS2, lp.getValidatedPrivateDnsServers().get(1)); + + lp.removeValidatedPrivateDnsServer(PRIVDNS1); + assertEquals(1, lp.getValidatedPrivateDnsServers().size()); + assertEquals(PRIVDNS2, lp.getValidatedPrivateDnsServers().get(0)); + + lp.addValidatedPrivateDnsServer(PRIVDNS6); + assertEquals(2, lp.getValidatedPrivateDnsServers().size()); + assertEquals(PRIVDNS2, lp.getValidatedPrivateDnsServers().get(0)); + assertEquals(PRIVDNS6, lp.getValidatedPrivateDnsServers().get(1)); + } + + @Test + public void testPcscfServers() { + final LinkProperties lp = new LinkProperties(); + final List pcscfServers = Arrays.asList(PCSCFV4); + lp.setPcscfServers(pcscfServers); + assertEquals(1, lp.getPcscfServers().size()); + assertEquals(PCSCFV4, lp.getPcscfServers().get(0)); + + lp.removePcscfServer(PCSCFV4); + assertEquals(0, lp.getPcscfServers().size()); + + lp.addPcscfServer(PCSCFV6); + assertEquals(1, lp.getPcscfServers().size()); + assertEquals(PCSCFV6, lp.getPcscfServers().get(0)); + } + + @Test + public void testTcpBufferSizes() { + final LinkProperties lp = makeTestObject(); + assertEquals(TCP_BUFFER_SIZES, lp.getTcpBufferSizes()); + + lp.setTcpBufferSizes(null); + assertNull(lp.getTcpBufferSizes()); + } + + @Test + public void testHasIpv6DefaultRoute() { + final LinkProperties lp = makeTestObject(); + assertFalse(lp.hasIPv6DefaultRoute()); + + lp.addRoute(new RouteInfo(GATEWAY61)); + assertTrue(lp.hasIPv6DefaultRoute()); + } + + @Test + public void testHttpProxy() { + final LinkProperties lp = makeTestObject(); + assertTrue(lp.getHttpProxy().equals(ProxyInfo.buildDirectProxy("test", 8888))); + } + + @Test + public void testPrivateDnsServerName() { + final LinkProperties lp = makeTestObject(); + assertEquals(PRIV_DNS_SERVER_NAME, lp.getPrivateDnsServerName()); + + lp.setPrivateDnsServerName(null); + assertNull(lp.getPrivateDnsServerName()); + } + + @Test + public void testUsePrivateDns() { + final LinkProperties lp = makeTestObject(); + assertTrue(lp.isPrivateDnsActive()); + + lp.clear(); + assertFalse(lp.isPrivateDnsActive()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testDhcpServerAddress() { + final LinkProperties lp = makeTestObject(); + assertEquals(DHCPSERVER, lp.getDhcpServerAddress()); + + lp.clear(); + assertNull(lp.getDhcpServerAddress()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testWakeOnLanSupported() { + final LinkProperties lp = makeTestObject(); + assertTrue(lp.isWakeOnLanSupported()); + + lp.clear(); + assertFalse(lp.isWakeOnLanSupported()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testCaptivePortalApiUrl() { + final LinkProperties lp = makeTestObject(); + assertEquals(CAPPORT_API_URL, lp.getCaptivePortalApiUrl()); + + lp.clear(); + assertNull(lp.getCaptivePortalApiUrl()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testCaptivePortalData() { + final LinkProperties lp = makeTestObject(); + assertEquals(getCaptivePortalData(), lp.getCaptivePortalData()); + + lp.clear(); + assertNull(lp.getCaptivePortalData()); + } + + private LinkProperties makeIpv4LinkProperties() { + final LinkProperties linkProperties = new LinkProperties(); + linkProperties.setInterfaceName(NAME); + linkProperties.addLinkAddress(LINKADDRV4); + linkProperties.addDnsServer(DNS1); + linkProperties.addRoute(new RouteInfo(GATEWAY1)); + linkProperties.addRoute(new RouteInfo(GATEWAY2)); + return linkProperties; + } + + private LinkProperties makeIpv6LinkProperties() { + final LinkProperties linkProperties = new LinkProperties(); + linkProperties.setInterfaceName(NAME); + linkProperties.addLinkAddress(LINKADDRV6); + linkProperties.addDnsServer(DNS6); + linkProperties.addRoute(new RouteInfo(GATEWAY61)); + linkProperties.addRoute(new RouteInfo(GATEWAY62)); + return linkProperties; + } + + @Test + public void testHasIpv4DefaultRoute() { + final LinkProperties Ipv4 = makeIpv4LinkProperties(); + assertTrue(Ipv4.hasIpv4DefaultRoute()); + final LinkProperties Ipv6 = makeIpv6LinkProperties(); + assertFalse(Ipv6.hasIpv4DefaultRoute()); + } + + @Test + public void testHasIpv4DnsServer() { + final LinkProperties Ipv4 = makeIpv4LinkProperties(); + assertTrue(Ipv4.hasIpv4DnsServer()); + final LinkProperties Ipv6 = makeIpv6LinkProperties(); + assertFalse(Ipv6.hasIpv4DnsServer()); + } + + @Test + public void testHasIpv6DnsServer() { + final LinkProperties Ipv4 = makeIpv4LinkProperties(); + assertFalse(Ipv4.hasIpv6DnsServer()); + final LinkProperties Ipv6 = makeIpv6LinkProperties(); + assertTrue(Ipv6.hasIpv6DnsServer()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testHasIpv4UnreachableDefaultRoute() { + final LinkProperties lp = makeTestObject(); + assertFalse(lp.hasIpv4UnreachableDefaultRoute()); + assertFalse(lp.hasIpv6UnreachableDefaultRoute()); + + lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), RTN_UNREACHABLE)); + assertTrue(lp.hasIpv4UnreachableDefaultRoute()); + assertFalse(lp.hasIpv6UnreachableDefaultRoute()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testHasIpv6UnreachableDefaultRoute() { + final LinkProperties lp = makeTestObject(); + assertFalse(lp.hasIpv6UnreachableDefaultRoute()); + assertFalse(lp.hasIpv4UnreachableDefaultRoute()); + + lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE)); + assertTrue(lp.hasIpv6UnreachableDefaultRoute()); + assertFalse(lp.hasIpv4UnreachableDefaultRoute()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testRouteAddWithSameKey() throws Exception { + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("wlan0"); + final IpPrefix v6 = new IpPrefix("64:ff9b::/96"); + lp.addRoute(new RouteInfo(v6, address("fe80::1"), "wlan0", RTN_UNICAST, 1280)); + assertEquals(1, lp.getRoutes().size()); + lp.addRoute(new RouteInfo(v6, address("fe80::1"), "wlan0", RTN_UNICAST, 1500)); + assertEquals(1, lp.getRoutes().size()); + final IpPrefix v4 = new IpPrefix("192.0.2.128/25"); + lp.addRoute(new RouteInfo(v4, address("192.0.2.1"), "wlan0", RTN_UNICAST, 1460)); + assertEquals(2, lp.getRoutes().size()); + lp.addRoute(new RouteInfo(v4, address("192.0.2.1"), "wlan0", RTN_THROW, 1460)); + assertEquals(2, lp.getRoutes().size()); + } +} diff --git a/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt b/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt new file mode 100644 index 0000000000..a5e44d59fc --- /dev/null +++ b/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2020 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 + +import android.net.wifi.aware.DiscoverySession +import android.net.wifi.aware.PeerHandle +import android.net.wifi.aware.WifiAwareNetworkSpecifier +import android.os.Build +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 + +import com.android.testutils.assertParcelSane +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo + +import java.lang.IllegalStateException + +import org.junit.Assert.assertFalse +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito + +@RunWith(AndroidJUnit4::class) +@SmallTest +class MatchAllNetworkSpecifierTest { + @Rule @JvmField + val ignoreRule: DevSdkIgnoreRule = DevSdkIgnoreRule() + + private val specifier = MatchAllNetworkSpecifier() + private val discoverySession = Mockito.mock(DiscoverySession::class.java) + private val peerHandle = Mockito.mock(PeerHandle::class.java) + private val wifiAwareNetworkSpecifier = WifiAwareNetworkSpecifier.Builder(discoverySession, + peerHandle).build() + + @Test + fun testParcel() { + assertParcelSane(MatchAllNetworkSpecifier(), 0) + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.Q) + @IgnoreAfter(Build.VERSION_CODES.R) + // Only run this test on Android R. + // The method - satisfiedBy() has changed to canBeSatisfiedBy() starting from Android R, so the + // method - canBeSatisfiedBy() cannot be found when running this test on Android Q. + fun testCanBeSatisfiedBy_OnlyForR() { + // MatchAllNetworkSpecifier didn't follow its parent class to change the satisfiedBy() to + // canBeSatisfiedBy(), so if a caller calls MatchAllNetworkSpecifier#canBeSatisfiedBy(), the + // NetworkSpecifier#canBeSatisfiedBy() will be called actually, and false will be returned. + // Although it's not meeting the expectation, the behavior still needs to be verified. + assertFalse(specifier.canBeSatisfiedBy(wifiAwareNetworkSpecifier)) + } + + @Test(expected = IllegalStateException::class) + @IgnoreUpTo(Build.VERSION_CODES.R) + fun testCanBeSatisfiedBy() { + specifier.canBeSatisfiedBy(wifiAwareNetworkSpecifier) + } +} diff --git a/tests/common/java/android/net/NattKeepalivePacketDataTest.kt b/tests/common/java/android/net/NattKeepalivePacketDataTest.kt new file mode 100644 index 0000000000..46f39dd016 --- /dev/null +++ b/tests/common/java/android/net/NattKeepalivePacketDataTest.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2020 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 + +import android.net.InvalidPacketException.ERROR_INVALID_IP_ADDRESS +import android.net.InvalidPacketException.ERROR_INVALID_PORT +import android.net.NattSocketKeepalive.NATT_PORT +import android.os.Build +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.assertEqualBothWays +import com.android.testutils.assertFieldCountEquals +import com.android.testutils.assertParcelSane +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import com.android.testutils.parcelingRoundTrip +import java.net.InetAddress +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.fail +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class NattKeepalivePacketDataTest { + @Rule @JvmField + val ignoreRule: DevSdkIgnoreRule = DevSdkIgnoreRule() + + /* Refer to the definition in {@code NattKeepalivePacketData} */ + private val IPV4_HEADER_LENGTH = 20 + private val UDP_HEADER_LENGTH = 8 + + private val TEST_PORT = 4243 + private val TEST_PORT2 = 4244 + private val TEST_SRC_ADDRV4 = "198.168.0.2".address() + private val TEST_DST_ADDRV4 = "198.168.0.1".address() + private val TEST_ADDRV6 = "2001:db8::1".address() + + private fun String.address() = InetAddresses.parseNumericAddress(this) + private fun nattKeepalivePacket( + srcAddress: InetAddress? = TEST_SRC_ADDRV4, + srcPort: Int = TEST_PORT, + dstAddress: InetAddress? = TEST_DST_ADDRV4, + dstPort: Int = NATT_PORT + ) = NattKeepalivePacketData.nattKeepalivePacket(srcAddress, srcPort, dstAddress, dstPort) + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testConstructor() { + try { + nattKeepalivePacket(dstPort = TEST_PORT) + fail("Dst port is not NATT port should cause exception") + } catch (e: InvalidPacketException) { + assertEquals(e.error, ERROR_INVALID_PORT) + } + + try { + nattKeepalivePacket(srcAddress = TEST_ADDRV6) + fail("A v6 srcAddress should cause exception") + } catch (e: InvalidPacketException) { + assertEquals(e.error, ERROR_INVALID_IP_ADDRESS) + } + + try { + nattKeepalivePacket(dstAddress = TEST_ADDRV6) + fail("A v6 dstAddress should cause exception") + } catch (e: InvalidPacketException) { + assertEquals(e.error, ERROR_INVALID_IP_ADDRESS) + } + + try { + parcelingRoundTrip( + NattKeepalivePacketData(TEST_SRC_ADDRV4, TEST_PORT, TEST_DST_ADDRV4, TEST_PORT, + byteArrayOf(12, 31, 22, 44))) + fail("Invalid data should cause exception") + } catch (e: IllegalArgumentException) { } + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testParcel() { + assertParcelSane(nattKeepalivePacket(), 0) + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testEquals() { + assertEqualBothWays(nattKeepalivePacket(), nattKeepalivePacket()) + assertNotEquals(nattKeepalivePacket(dstAddress = TEST_SRC_ADDRV4), nattKeepalivePacket()) + assertNotEquals(nattKeepalivePacket(srcAddress = TEST_DST_ADDRV4), nattKeepalivePacket()) + // Test src port only because dst port have to be NATT_PORT + assertNotEquals(nattKeepalivePacket(srcPort = TEST_PORT2), nattKeepalivePacket()) + // Make sure the parceling test is updated if fields are added in the base class. + assertFieldCountEquals(5, KeepalivePacketData::class.java) + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testHashCode() { + assertEquals(nattKeepalivePacket().hashCode(), nattKeepalivePacket().hashCode()) + } +} \ No newline at end of file diff --git a/tests/common/java/android/net/NetworkAgentConfigTest.kt b/tests/common/java/android/net/NetworkAgentConfigTest.kt new file mode 100644 index 0000000000..2b45b3d69c --- /dev/null +++ b/tests/common/java/android/net/NetworkAgentConfigTest.kt @@ -0,0 +1,91 @@ +/* + * 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 + +import android.os.Build +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.modules.utils.build.SdkLevel.isAtLeastS +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import com.android.testutils.assertParcelSane +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class NetworkAgentConfigTest { + @Rule @JvmField + val ignoreRule = DevSdkIgnoreRule() + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testParcelNetworkAgentConfig() { + val config = NetworkAgentConfig.Builder().apply { + setExplicitlySelected(true) + setLegacyType(ConnectivityManager.TYPE_ETHERNET) + setSubscriberId("MySubId") + setPartialConnectivityAcceptable(false) + setUnvalidatedConnectivityAcceptable(true) + if (isAtLeastS()) { + setBypassableVpn(true) + } + }.build() + if (isAtLeastS()) { + // From S, the config will have 12 items + assertParcelSane(config, 12) + } else { + // For R or below, the config will have 10 items + assertParcelSane(config, 10) + } + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testBuilder() { + val config = NetworkAgentConfig.Builder().apply { + setExplicitlySelected(true) + setLegacyType(ConnectivityManager.TYPE_ETHERNET) + setSubscriberId("MySubId") + setPartialConnectivityAcceptable(false) + setUnvalidatedConnectivityAcceptable(true) + setLegacyTypeName("TEST_NETWORK") + if (isAtLeastS()) { + setNat64DetectionEnabled(false) + setProvisioningNotificationEnabled(false) + setBypassableVpn(true) + } + }.build() + + assertTrue(config.isExplicitlySelected()) + assertEquals(ConnectivityManager.TYPE_ETHERNET, config.getLegacyType()) + assertEquals("MySubId", config.getSubscriberId()) + assertFalse(config.isPartialConnectivityAcceptable()) + assertTrue(config.isUnvalidatedConnectivityAcceptable()) + assertEquals("TEST_NETWORK", config.getLegacyTypeName()) + if (isAtLeastS()) { + assertFalse(config.isNat64DetectionEnabled()) + assertFalse(config.isProvisioningNotificationEnabled()) + assertTrue(config.isBypassableVpn()) + } else { + assertTrue(config.isNat64DetectionEnabled()) + assertTrue(config.isProvisioningNotificationEnabled()) + } + } +} diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java new file mode 100644 index 0000000000..b178bad712 --- /dev/null +++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java @@ -0,0 +1,1152 @@ +/* + * 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 android.net; + +import static android.net.NetworkCapabilities.LINK_BANDWIDTH_UNSPECIFIED; +import static android.net.NetworkCapabilities.MAX_TRANSPORT; +import static android.net.NetworkCapabilities.MIN_TRANSPORT; +import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL; +import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_EIMS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; +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_VCN_MANAGED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID; +import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE; +import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY; +import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P; +import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION; +import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS; +import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS; +import static android.net.NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_TEST; +import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE; +import static android.os.Process.INVALID_UID; + +import static com.android.modules.utils.build.SdkLevel.isAtLeastR; +import static com.android.modules.utils.build.SdkLevel.isAtLeastS; +import static com.android.testutils.MiscAsserts.assertEmpty; +import static com.android.testutils.MiscAsserts.assertThrows; +import static com.android.testutils.ParcelUtils.assertParcelSane; +import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; + +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.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; + +import android.net.wifi.aware.DiscoverySession; +import android.net.wifi.aware.PeerHandle; +import android.net.wifi.aware.WifiAwareNetworkSpecifier; +import android.os.Build; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.ArraySet; +import android.util.Range; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.CompatUtil; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; + +import java.util.Arrays; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkCapabilitiesTest { + private static final String TEST_SSID = "TEST_SSID"; + private static final String DIFFERENT_TEST_SSID = "DIFFERENT_TEST_SSID"; + private static final int TEST_SUBID1 = 1; + private static final int TEST_SUBID2 = 2; + private static final int TEST_SUBID3 = 3; + + @Rule + public DevSdkIgnoreRule mDevSdkIgnoreRule = new DevSdkIgnoreRule(); + + private DiscoverySession mDiscoverySession = Mockito.mock(DiscoverySession.class); + private PeerHandle mPeerHandle = Mockito.mock(PeerHandle.class); + + @Test + public void testMaybeMarkCapabilitiesRestricted() { + // check that internet does not get restricted + NetworkCapabilities netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_INTERNET); + netCap.maybeMarkCapabilitiesRestricted(); + assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + // metered-ness shouldn't matter + netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_INTERNET); + netCap.addCapability(NET_CAPABILITY_NOT_METERED); + netCap.maybeMarkCapabilitiesRestricted(); + assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_INTERNET); + netCap.removeCapability(NET_CAPABILITY_NOT_METERED); + netCap.maybeMarkCapabilitiesRestricted(); + assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + // add EIMS - bundled with unrestricted means it's unrestricted + netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_INTERNET); + netCap.addCapability(NET_CAPABILITY_EIMS); + netCap.addCapability(NET_CAPABILITY_NOT_METERED); + netCap.maybeMarkCapabilitiesRestricted(); + assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_INTERNET); + netCap.addCapability(NET_CAPABILITY_EIMS); + netCap.removeCapability(NET_CAPABILITY_NOT_METERED); + netCap.maybeMarkCapabilitiesRestricted(); + assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + // just a restricted cap should be restricted regardless of meteredness + netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_EIMS); + netCap.addCapability(NET_CAPABILITY_NOT_METERED); + netCap.maybeMarkCapabilitiesRestricted(); + assertFalse(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_EIMS); + netCap.removeCapability(NET_CAPABILITY_NOT_METERED); + netCap.maybeMarkCapabilitiesRestricted(); + assertFalse(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + // try 2 restricted caps + netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_CBS); + netCap.addCapability(NET_CAPABILITY_EIMS); + netCap.addCapability(NET_CAPABILITY_NOT_METERED); + netCap.maybeMarkCapabilitiesRestricted(); + assertFalse(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_CBS); + netCap.addCapability(NET_CAPABILITY_EIMS); + netCap.removeCapability(NET_CAPABILITY_NOT_METERED); + netCap.maybeMarkCapabilitiesRestricted(); + assertFalse(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + } + + @Test + public void testDescribeImmutableDifferences() { + NetworkCapabilities nc1; + NetworkCapabilities nc2; + + // Transports changing + nc1 = new NetworkCapabilities().addTransportType(TRANSPORT_CELLULAR); + nc2 = new NetworkCapabilities().addTransportType(TRANSPORT_WIFI); + assertNotEquals("", nc1.describeImmutableDifferences(nc2)); + assertEquals("", nc1.describeImmutableDifferences(nc1)); + + // Mutable capability changing + nc1 = new NetworkCapabilities().addCapability(NET_CAPABILITY_VALIDATED); + nc2 = new NetworkCapabilities(); + assertEquals("", nc1.describeImmutableDifferences(nc2)); + assertEquals("", nc1.describeImmutableDifferences(nc1)); + + // NOT_METERED changing (http://b/63326103) + nc1 = new NetworkCapabilities() + .addCapability(NET_CAPABILITY_NOT_METERED) + .addCapability(NET_CAPABILITY_INTERNET); + nc2 = new NetworkCapabilities().addCapability(NET_CAPABILITY_INTERNET); + assertEquals("", nc1.describeImmutableDifferences(nc2)); + assertEquals("", nc1.describeImmutableDifferences(nc1)); + + // Immutable capability changing + nc1 = new NetworkCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + nc2 = new NetworkCapabilities().addCapability(NET_CAPABILITY_INTERNET); + assertNotEquals("", nc1.describeImmutableDifferences(nc2)); + assertEquals("", nc1.describeImmutableDifferences(nc1)); + + // Specifier changing + nc1 = new NetworkCapabilities().addTransportType(TRANSPORT_WIFI); + nc2 = new NetworkCapabilities() + .addTransportType(TRANSPORT_WIFI) + .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier("eth42")); + assertNotEquals("", nc1.describeImmutableDifferences(nc2)); + assertEquals("", nc1.describeImmutableDifferences(nc1)); + } + + @Test + public void testLinkBandwidthUtils() { + assertEquals(LINK_BANDWIDTH_UNSPECIFIED, NetworkCapabilities + .minBandwidth(LINK_BANDWIDTH_UNSPECIFIED, LINK_BANDWIDTH_UNSPECIFIED)); + assertEquals(10, NetworkCapabilities + .minBandwidth(LINK_BANDWIDTH_UNSPECIFIED, 10)); + assertEquals(10, NetworkCapabilities + .minBandwidth(10, LINK_BANDWIDTH_UNSPECIFIED)); + assertEquals(10, NetworkCapabilities + .minBandwidth(10, 20)); + + assertEquals(LINK_BANDWIDTH_UNSPECIFIED, NetworkCapabilities + .maxBandwidth(LINK_BANDWIDTH_UNSPECIFIED, LINK_BANDWIDTH_UNSPECIFIED)); + assertEquals(10, NetworkCapabilities + .maxBandwidth(LINK_BANDWIDTH_UNSPECIFIED, 10)); + assertEquals(10, NetworkCapabilities + .maxBandwidth(10, LINK_BANDWIDTH_UNSPECIFIED)); + assertEquals(20, NetworkCapabilities + .maxBandwidth(10, 20)); + } + + @Test + public void testSetUids() { + final NetworkCapabilities netCap = new NetworkCapabilities(); + // Null uids match all UIDs + netCap.setUids(null); + assertTrue(netCap.appliesToUid(10)); + assertTrue(netCap.appliesToUid(200)); + assertTrue(netCap.appliesToUid(3000)); + assertTrue(netCap.appliesToUid(10010)); + assertTrue(netCap.appliesToUidRange(new UidRange(50, 100))); + assertTrue(netCap.appliesToUidRange(new UidRange(70, 72))); + assertTrue(netCap.appliesToUidRange(new UidRange(3500, 3912))); + assertTrue(netCap.appliesToUidRange(new UidRange(1, 100000))); + + if (isAtLeastS()) { + final Set> uids = new ArraySet<>(); + uids.add(uidRange(50, 100)); + uids.add(uidRange(3000, 4000)); + netCap.setUids(uids); + assertTrue(netCap.appliesToUid(50)); + assertTrue(netCap.appliesToUid(80)); + assertTrue(netCap.appliesToUid(100)); + assertTrue(netCap.appliesToUid(3000)); + assertTrue(netCap.appliesToUid(3001)); + assertFalse(netCap.appliesToUid(10)); + assertFalse(netCap.appliesToUid(25)); + assertFalse(netCap.appliesToUid(49)); + assertFalse(netCap.appliesToUid(101)); + assertFalse(netCap.appliesToUid(2000)); + assertFalse(netCap.appliesToUid(100000)); + + assertTrue(netCap.appliesToUidRange(new UidRange(50, 100))); + assertTrue(netCap.appliesToUidRange(new UidRange(70, 72))); + assertTrue(netCap.appliesToUidRange(new UidRange(3500, 3912))); + assertFalse(netCap.appliesToUidRange(new UidRange(1, 100))); + assertFalse(netCap.appliesToUidRange(new UidRange(49, 100))); + assertFalse(netCap.appliesToUidRange(new UidRange(1, 10))); + assertFalse(netCap.appliesToUidRange(new UidRange(60, 101))); + assertFalse(netCap.appliesToUidRange(new UidRange(60, 3400))); + + NetworkCapabilities netCap2 = new NetworkCapabilities(); + // A new netcap object has null UIDs, so anything will satisfy it. + assertTrue(netCap2.satisfiedByUids(netCap)); + // Still not equal though. + assertFalse(netCap2.equalsUids(netCap)); + netCap2.setUids(uids); + assertTrue(netCap2.satisfiedByUids(netCap)); + assertTrue(netCap.equalsUids(netCap2)); + assertTrue(netCap2.equalsUids(netCap)); + + uids.add(uidRange(600, 700)); + netCap2.setUids(uids); + assertFalse(netCap2.satisfiedByUids(netCap)); + assertFalse(netCap.appliesToUid(650)); + assertTrue(netCap2.appliesToUid(650)); + netCap.combineCapabilities(netCap2); + assertTrue(netCap2.satisfiedByUids(netCap)); + assertTrue(netCap.appliesToUid(650)); + assertFalse(netCap.appliesToUid(500)); + + assertTrue(new NetworkCapabilities().satisfiedByUids(netCap)); + netCap.combineCapabilities(new NetworkCapabilities()); + assertTrue(netCap.appliesToUid(500)); + assertTrue(netCap.appliesToUidRange(new UidRange(1, 100000))); + assertFalse(netCap2.appliesToUid(500)); + assertFalse(netCap2.appliesToUidRange(new UidRange(1, 100000))); + assertTrue(new NetworkCapabilities().satisfiedByUids(netCap)); + + // Null uids satisfies everything. + netCap.setUids(null); + assertTrue(netCap2.satisfiedByUids(netCap)); + assertTrue(netCap.satisfiedByUids(netCap2)); + netCap2.setUids(null); + assertTrue(netCap2.satisfiedByUids(netCap)); + assertTrue(netCap.satisfiedByUids(netCap2)); + } + } + + @Test + public void testParcelNetworkCapabilities() { + final Set> uids = new ArraySet<>(); + uids.add(uidRange(50, 100)); + uids.add(uidRange(3000, 4000)); + final NetworkCapabilities netCap = new NetworkCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .addCapability(NET_CAPABILITY_EIMS) + .addCapability(NET_CAPABILITY_NOT_METERED); + if (isAtLeastS()) { + netCap.setSubscriptionIds(Set.of(TEST_SUBID1, TEST_SUBID2)); + netCap.setUids(uids); + } + if (isAtLeastR()) { + netCap.setOwnerUid(123); + netCap.setAdministratorUids(new int[] {5, 11}); + } + assertParcelingIsLossless(netCap); + netCap.setSSID(TEST_SSID); + testParcelSane(netCap); + } + + @Test + public void testParcelNetworkCapabilitiesWithRequestorUidAndPackageName() { + final NetworkCapabilities netCap = new NetworkCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .addCapability(NET_CAPABILITY_EIMS) + .addCapability(NET_CAPABILITY_NOT_METERED); + if (isAtLeastR()) { + netCap.setRequestorPackageName("com.android.test"); + netCap.setRequestorUid(9304); + } + assertParcelingIsLossless(netCap); + netCap.setSSID(TEST_SSID); + testParcelSane(netCap); + } + + private void testParcelSane(NetworkCapabilities cap) { + if (isAtLeastS()) { + assertParcelSane(cap, 17); + } else if (isAtLeastR()) { + assertParcelSane(cap, 15); + } else { + assertParcelSane(cap, 11); + } + } + + private static NetworkCapabilities createNetworkCapabilitiesWithTransportInfo() { + return new NetworkCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .addCapability(NET_CAPABILITY_EIMS) + .addCapability(NET_CAPABILITY_NOT_METERED) + .setSSID(TEST_SSID) + .setTransportInfo(new TestTransportInfo()) + .setRequestorPackageName("com.android.test") + .setRequestorUid(9304); + } + + @Test + public void testNetworkCapabilitiesCopyWithNoRedactions() { + assumeTrue(isAtLeastS()); + + final NetworkCapabilities netCap = createNetworkCapabilitiesWithTransportInfo(); + final NetworkCapabilities netCapWithNoRedactions = + new NetworkCapabilities(netCap, NetworkCapabilities.REDACT_NONE); + TestTransportInfo testTransportInfo = + (TestTransportInfo) netCapWithNoRedactions.getTransportInfo(); + assertFalse(testTransportInfo.locationRedacted); + assertFalse(testTransportInfo.localMacAddressRedacted); + assertFalse(testTransportInfo.settingsRedacted); + } + + @Test + public void testNetworkCapabilitiesCopyWithoutLocationSensitiveFields() { + assumeTrue(isAtLeastS()); + + final NetworkCapabilities netCap = createNetworkCapabilitiesWithTransportInfo(); + final NetworkCapabilities netCapWithNoRedactions = + new NetworkCapabilities(netCap, REDACT_FOR_ACCESS_FINE_LOCATION); + TestTransportInfo testTransportInfo = + (TestTransportInfo) netCapWithNoRedactions.getTransportInfo(); + assertTrue(testTransportInfo.locationRedacted); + assertFalse(testTransportInfo.localMacAddressRedacted); + assertFalse(testTransportInfo.settingsRedacted); + } + + @Test + public void testOemPaid() { + NetworkCapabilities nc = new NetworkCapabilities(); + // By default OEM_PAID is neither in the required or forbidden lists and the network is not + // restricted. + if (isAtLeastS()) { + assertFalse(nc.hasForbiddenCapability(NET_CAPABILITY_OEM_PAID)); + } + assertFalse(nc.hasCapability(NET_CAPABILITY_OEM_PAID)); + nc.maybeMarkCapabilitiesRestricted(); + assertTrue(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + // Adding OEM_PAID to capability list should make network restricted. + nc.addCapability(NET_CAPABILITY_OEM_PAID); + nc.addCapability(NET_CAPABILITY_INTERNET); // Combine with unrestricted capability. + nc.maybeMarkCapabilitiesRestricted(); + assertTrue(nc.hasCapability(NET_CAPABILITY_OEM_PAID)); + assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + // Now let's make request for OEM_PAID network. + NetworkCapabilities nr = new NetworkCapabilities(); + nr.addCapability(NET_CAPABILITY_OEM_PAID); + nr.maybeMarkCapabilitiesRestricted(); + assertTrue(nr.satisfiedByNetworkCapabilities(nc)); + + // Request fails for network with the default capabilities. + assertFalse(nr.satisfiedByNetworkCapabilities(new NetworkCapabilities())); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + public void testOemPrivate() { + NetworkCapabilities nc = new NetworkCapabilities(); + // By default OEM_PRIVATE is neither in the required or forbidden lists and the network is + // not restricted. + assertFalse(nc.hasForbiddenCapability(NET_CAPABILITY_OEM_PRIVATE)); + assertFalse(nc.hasCapability(NET_CAPABILITY_OEM_PRIVATE)); + nc.maybeMarkCapabilitiesRestricted(); + assertTrue(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + // Adding OEM_PRIVATE to capability list should make network restricted. + nc.addCapability(NET_CAPABILITY_OEM_PRIVATE); + nc.addCapability(NET_CAPABILITY_INTERNET); // Combine with unrestricted capability. + nc.maybeMarkCapabilitiesRestricted(); + assertTrue(nc.hasCapability(NET_CAPABILITY_OEM_PRIVATE)); + assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + // Now let's make request for OEM_PRIVATE network. + NetworkCapabilities nr = new NetworkCapabilities(); + nr.addCapability(NET_CAPABILITY_OEM_PRIVATE); + nr.maybeMarkCapabilitiesRestricted(); + assertTrue(nr.satisfiedByNetworkCapabilities(nc)); + + // Request fails for network with the default capabilities. + assertFalse(nr.satisfiedByNetworkCapabilities(new NetworkCapabilities())); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + public void testForbiddenCapabilities() { + NetworkCapabilities network = new NetworkCapabilities(); + + NetworkCapabilities request = new NetworkCapabilities(); + assertTrue("Request: " + request + ", Network:" + network, + request.satisfiedByNetworkCapabilities(network)); + + // Requesting absence of capabilities that network doesn't have. Request should satisfy. + request.addForbiddenCapability(NET_CAPABILITY_WIFI_P2P); + request.addForbiddenCapability(NET_CAPABILITY_NOT_METERED); + assertTrue(request.satisfiedByNetworkCapabilities(network)); + assertArrayEquals(new int[]{NET_CAPABILITY_WIFI_P2P, + NET_CAPABILITY_NOT_METERED}, + request.getForbiddenCapabilities()); + + // This is a default capability, just want to make sure its there because we use it below. + assertTrue(network.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + // Verify that adding forbidden capability will effectively remove it from capability list. + request.addForbiddenCapability(NET_CAPABILITY_NOT_RESTRICTED); + assertTrue(request.hasForbiddenCapability(NET_CAPABILITY_NOT_RESTRICTED)); + assertFalse(request.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + // Now this request won't be satisfied because network contains NOT_RESTRICTED. + assertFalse(request.satisfiedByNetworkCapabilities(network)); + network.removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + assertTrue(request.satisfiedByNetworkCapabilities(network)); + + // Verify that adding capability will effectively remove it from forbidden list + request.addCapability(NET_CAPABILITY_NOT_RESTRICTED); + assertTrue(request.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + assertFalse(request.hasForbiddenCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + assertFalse(request.satisfiedByNetworkCapabilities(network)); + network.addCapability(NET_CAPABILITY_NOT_RESTRICTED); + assertTrue(request.satisfiedByNetworkCapabilities(network)); + } + + @Test + public void testConnectivityManagedCapabilities() { + NetworkCapabilities nc = new NetworkCapabilities(); + assertFalse(nc.hasConnectivityManagedCapability()); + // Check every single system managed capability. + nc.addCapability(NET_CAPABILITY_CAPTIVE_PORTAL); + assertTrue(nc.hasConnectivityManagedCapability()); + nc.removeCapability(NET_CAPABILITY_CAPTIVE_PORTAL); + nc.addCapability(NET_CAPABILITY_FOREGROUND); + assertTrue(nc.hasConnectivityManagedCapability()); + nc.removeCapability(NET_CAPABILITY_FOREGROUND); + nc.addCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY); + assertTrue(nc.hasConnectivityManagedCapability()); + nc.removeCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY); + nc.addCapability(NET_CAPABILITY_VALIDATED); + assertTrue(nc.hasConnectivityManagedCapability()); + } + + @Test + public void testEqualsNetCapabilities() { + NetworkCapabilities nc1 = new NetworkCapabilities(); + NetworkCapabilities nc2 = new NetworkCapabilities(); + assertTrue(nc1.equalsNetCapabilities(nc2)); + assertEquals(nc1, nc2); + + nc1.addCapability(NET_CAPABILITY_MMS); + assertFalse(nc1.equalsNetCapabilities(nc2)); + assertNotEquals(nc1, nc2); + nc2.addCapability(NET_CAPABILITY_MMS); + assertTrue(nc1.equalsNetCapabilities(nc2)); + assertEquals(nc1, nc2); + + if (isAtLeastS()) { + nc1.addForbiddenCapability(NET_CAPABILITY_INTERNET); + assertFalse(nc1.equalsNetCapabilities(nc2)); + nc2.addForbiddenCapability(NET_CAPABILITY_INTERNET); + assertTrue(nc1.equalsNetCapabilities(nc2)); + + // Remove a required capability doesn't affect forbidden capabilities. + // This is a behaviour change from R to S. + nc1.removeCapability(NET_CAPABILITY_INTERNET); + assertTrue(nc1.equalsNetCapabilities(nc2)); + + nc1.removeForbiddenCapability(NET_CAPABILITY_INTERNET); + assertFalse(nc1.equalsNetCapabilities(nc2)); + nc2.removeForbiddenCapability(NET_CAPABILITY_INTERNET); + assertTrue(nc1.equalsNetCapabilities(nc2)); + } + } + + @Test + public void testSSID() { + NetworkCapabilities nc1 = new NetworkCapabilities(); + NetworkCapabilities nc2 = new NetworkCapabilities(); + assertTrue(nc2.satisfiedBySSID(nc1)); + + nc1.setSSID(TEST_SSID); + assertTrue(nc2.satisfiedBySSID(nc1)); + nc2.setSSID("different " + TEST_SSID); + assertFalse(nc2.satisfiedBySSID(nc1)); + + assertTrue(nc1.satisfiedByImmutableNetworkCapabilities(nc2)); + assertFalse(nc1.satisfiedByNetworkCapabilities(nc2)); + } + + private ArraySet> uidRanges(int from, int to) { + final ArraySet> range = new ArraySet<>(1); + range.add(uidRange(from, to)); + return range; + } + + private Range uidRange(int from, int to) { + return new Range(from, to); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testSetAdministratorUids() { + NetworkCapabilities nc = + new NetworkCapabilities().setAdministratorUids(new int[] {2, 1, 3}); + + assertArrayEquals(new int[] {1, 2, 3}, nc.getAdministratorUids()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testSetAdministratorUidsWithDuplicates() { + try { + new NetworkCapabilities().setAdministratorUids(new int[] {1, 1}); + fail("Expected IllegalArgumentException for duplicate uids"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testCombineCapabilities() { + NetworkCapabilities nc1 = new NetworkCapabilities(); + NetworkCapabilities nc2 = new NetworkCapabilities(); + + if (isAtLeastS()) { + nc1.addForbiddenCapability(NET_CAPABILITY_CAPTIVE_PORTAL); + } + nc1.addCapability(NET_CAPABILITY_NOT_ROAMING); + assertNotEquals(nc1, nc2); + nc2.combineCapabilities(nc1); + assertEquals(nc1, nc2); + assertTrue(nc2.hasCapability(NET_CAPABILITY_NOT_ROAMING)); + if (isAtLeastS()) { + assertTrue(nc2.hasForbiddenCapability(NET_CAPABILITY_CAPTIVE_PORTAL)); + } + + if (isAtLeastS()) { + // This will effectively move NOT_ROAMING capability from required to forbidden for nc1. + nc1.addForbiddenCapability(NET_CAPABILITY_NOT_ROAMING); + // It is not allowed to have the same capability in both wanted and forbidden list. + assertThrows(IllegalArgumentException.class, () -> nc2.combineCapabilities(nc1)); + // Remove forbidden capability to continue other tests. + nc1.removeForbiddenCapability(NET_CAPABILITY_NOT_ROAMING); + } + + nc1.setSSID(TEST_SSID); + nc2.combineCapabilities(nc1); + if (isAtLeastR()) { + assertTrue(TEST_SSID.equals(nc2.getSsid())); + } + + // Because they now have the same SSID, the following call should not throw + nc2.combineCapabilities(nc1); + + nc1.setSSID(DIFFERENT_TEST_SSID); + try { + nc2.combineCapabilities(nc1); + fail("Expected IllegalStateException: can't combine different SSIDs"); + } catch (IllegalStateException expected) {} + nc1.setSSID(TEST_SSID); + + if (isAtLeastS()) { + nc1.setUids(uidRanges(10, 13)); + assertNotEquals(nc1, nc2); + nc2.combineCapabilities(nc1); // Everything + 10~13 is still everything. + assertNotEquals(nc1, nc2); + nc1.combineCapabilities(nc2); // 10~13 + everything is everything. + assertEquals(nc1, nc2); + nc1.setUids(uidRanges(10, 13)); + nc2.setUids(uidRanges(20, 23)); + assertNotEquals(nc1, nc2); + nc1.combineCapabilities(nc2); + assertTrue(nc1.appliesToUid(12)); + assertFalse(nc2.appliesToUid(12)); + assertTrue(nc1.appliesToUid(22)); + assertTrue(nc2.appliesToUid(22)); + + // Verify the subscription id list can be combined only when they are equal. + nc1.setSubscriptionIds(Set.of(TEST_SUBID1, TEST_SUBID2)); + nc2.setSubscriptionIds(Set.of(TEST_SUBID2)); + assertThrows(IllegalStateException.class, () -> nc2.combineCapabilities(nc1)); + + nc2.setSubscriptionIds(Set.of()); + assertThrows(IllegalStateException.class, () -> nc2.combineCapabilities(nc1)); + + nc2.setSubscriptionIds(Set.of(TEST_SUBID2, TEST_SUBID1)); + nc2.combineCapabilities(nc1); + assertEquals(Set.of(TEST_SUBID2, TEST_SUBID1), nc2.getSubscriptionIds()); + } + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testCombineCapabilities_AdministratorUids() { + final NetworkCapabilities nc1 = new NetworkCapabilities(); + final NetworkCapabilities nc2 = new NetworkCapabilities(); + + final int[] adminUids = {3, 6, 12}; + nc1.setAdministratorUids(adminUids); + nc2.combineCapabilities(nc1); + assertTrue(nc2.equalsAdministratorUids(nc1)); + assertArrayEquals(nc2.getAdministratorUids(), adminUids); + + final int[] adminUidsOtherOrder = {3, 12, 6}; + nc1.setAdministratorUids(adminUidsOtherOrder); + assertTrue(nc2.equalsAdministratorUids(nc1)); + + final int[] adminUids2 = {11, 1, 12, 3, 6}; + nc1.setAdministratorUids(adminUids2); + assertFalse(nc2.equalsAdministratorUids(nc1)); + assertFalse(Arrays.equals(nc2.getAdministratorUids(), adminUids2)); + try { + nc2.combineCapabilities(nc1); + fail("Shouldn't be able to combine different lists of admin UIDs"); + } catch (IllegalStateException expected) { } + } + + @Test + public void testSetCapabilities() { + final int[] REQUIRED_CAPABILITIES = new int[] { + NET_CAPABILITY_INTERNET, NET_CAPABILITY_NOT_VPN }; + + NetworkCapabilities nc1 = new NetworkCapabilities(); + NetworkCapabilities nc2 = new NetworkCapabilities(); + + nc1.setCapabilities(REQUIRED_CAPABILITIES); + assertArrayEquals(REQUIRED_CAPABILITIES, nc1.getCapabilities()); + + // Verify that setting and adding capabilities leads to the same object state. + nc2.clearAll(); + for (int cap : REQUIRED_CAPABILITIES) { + nc2.addCapability(cap); + } + assertEquals(nc1, nc2); + + if (isAtLeastS()) { + final int[] forbiddenCapabilities = new int[]{ + NET_CAPABILITY_NOT_METERED, NET_CAPABILITY_NOT_RESTRICTED }; + + nc1.setCapabilities(REQUIRED_CAPABILITIES, forbiddenCapabilities); + assertArrayEquals(REQUIRED_CAPABILITIES, nc1.getCapabilities()); + assertArrayEquals(forbiddenCapabilities, nc1.getForbiddenCapabilities()); + + nc2.clearAll(); + for (int cap : REQUIRED_CAPABILITIES) { + nc2.addCapability(cap); + } + for (int cap : forbiddenCapabilities) { + nc2.addForbiddenCapability(cap); + } + assertEquals(nc1, nc2); + } + } + + @Test + public void testSetNetworkSpecifierOnMultiTransportNc() { + // Sequence 1: Transport + Transport + NetworkSpecifier + NetworkCapabilities nc1 = new NetworkCapabilities(); + nc1.addTransportType(TRANSPORT_CELLULAR).addTransportType(TRANSPORT_WIFI); + try { + nc1.setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier("eth0")); + fail("Cannot set NetworkSpecifier on a NetworkCapability with multiple transports!"); + } catch (IllegalStateException expected) { + // empty + } + + // Sequence 2: Transport + NetworkSpecifier + Transport + NetworkCapabilities nc2 = new NetworkCapabilities(); + nc2.addTransportType(TRANSPORT_CELLULAR).setNetworkSpecifier( + CompatUtil.makeEthernetNetworkSpecifier("testtap3")); + try { + nc2.addTransportType(TRANSPORT_WIFI); + fail("Cannot set a second TransportType of a network which has a NetworkSpecifier!"); + } catch (IllegalStateException expected) { + // empty + } + } + + @Test + public void testSetTransportInfoOnMultiTransportNc() { + // Sequence 1: Transport + Transport + TransportInfo + NetworkCapabilities nc1 = new NetworkCapabilities(); + nc1.addTransportType(TRANSPORT_CELLULAR).addTransportType(TRANSPORT_WIFI) + .setTransportInfo(new TestTransportInfo()); + + // Sequence 2: Transport + NetworkSpecifier + Transport + NetworkCapabilities nc2 = new NetworkCapabilities(); + nc2.addTransportType(TRANSPORT_CELLULAR).setTransportInfo(new TestTransportInfo()) + .addTransportType(TRANSPORT_WIFI); + } + + @Test + public void testCombineTransportInfo() { + NetworkCapabilities nc1 = new NetworkCapabilities(); + nc1.setTransportInfo(new TestTransportInfo()); + + NetworkCapabilities nc2 = new NetworkCapabilities(); + // new TransportInfo so that object is not #equals to nc1's TransportInfo (that's where + // combine fails) + nc2.setTransportInfo(new TestTransportInfo()); + + try { + nc1.combineCapabilities(nc2); + fail("Should not be able to combine NetworkCabilities which contain TransportInfos"); + } catch (IllegalStateException expected) { + // empty + } + + // verify that can combine with identical TransportInfo objects + NetworkCapabilities nc3 = new NetworkCapabilities(); + nc3.setTransportInfo(nc1.getTransportInfo()); + nc1.combineCapabilities(nc3); + } + + @Test + public void testSet() { + NetworkCapabilities nc1 = new NetworkCapabilities(); + NetworkCapabilities nc2 = new NetworkCapabilities(); + + if (isAtLeastS()) { + nc1.addForbiddenCapability(NET_CAPABILITY_CAPTIVE_PORTAL); + } + nc1.addCapability(NET_CAPABILITY_NOT_ROAMING); + assertNotEquals(nc1, nc2); + nc2.set(nc1); + assertEquals(nc1, nc2); + assertTrue(nc2.hasCapability(NET_CAPABILITY_NOT_ROAMING)); + if (isAtLeastS()) { + assertTrue(nc2.hasForbiddenCapability(NET_CAPABILITY_CAPTIVE_PORTAL)); + } + + if (isAtLeastS()) { + // This will effectively move NOT_ROAMING capability from required to forbidden for nc1. + nc1.addForbiddenCapability(NET_CAPABILITY_NOT_ROAMING); + } + nc1.setSSID(TEST_SSID); + nc2.set(nc1); + assertEquals(nc1, nc2); + if (isAtLeastS()) { + // Contrary to combineCapabilities, set() will have removed the NOT_ROAMING capability + // from nc2. + assertFalse(nc2.hasCapability(NET_CAPABILITY_NOT_ROAMING)); + assertTrue(nc2.hasForbiddenCapability(NET_CAPABILITY_NOT_ROAMING)); + } + + if (isAtLeastR()) { + assertTrue(TEST_SSID.equals(nc2.getSsid())); + } + + nc1.setSSID(DIFFERENT_TEST_SSID); + nc2.set(nc1); + assertEquals(nc1, nc2); + if (isAtLeastR()) { + assertTrue(DIFFERENT_TEST_SSID.equals(nc2.getSsid())); + } + if (isAtLeastS()) { + nc1.setUids(uidRanges(10, 13)); + } else { + nc1.setUids(null); + } + nc2.set(nc1); // Overwrites, as opposed to combineCapabilities + assertEquals(nc1, nc2); + + if (isAtLeastS()) { + assertThrows(NullPointerException.class, () -> nc1.setSubscriptionIds(null)); + nc1.setSubscriptionIds(Set.of()); + nc2.set(nc1); + assertEquals(nc1, nc2); + + nc1.setSubscriptionIds(Set.of(TEST_SUBID1)); + nc2.set(nc1); + assertEquals(nc1, nc2); + + nc2.setSubscriptionIds(Set.of(TEST_SUBID2, TEST_SUBID1)); + nc2.set(nc1); + assertEquals(nc1, nc2); + + nc2.setSubscriptionIds(Set.of(TEST_SUBID3, TEST_SUBID2)); + assertNotEquals(nc1, nc2); + } + } + + @Test + public void testGetTransportTypes() { + final NetworkCapabilities nc = new NetworkCapabilities(); + nc.addTransportType(TRANSPORT_CELLULAR); + nc.addTransportType(TRANSPORT_WIFI); + nc.addTransportType(TRANSPORT_VPN); + nc.addTransportType(TRANSPORT_TEST); + + final int[] transportTypes = nc.getTransportTypes(); + assertEquals(4, transportTypes.length); + assertEquals(TRANSPORT_CELLULAR, transportTypes[0]); + assertEquals(TRANSPORT_WIFI, transportTypes[1]); + assertEquals(TRANSPORT_VPN, transportTypes[2]); + assertEquals(TRANSPORT_TEST, transportTypes[3]); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testTelephonyNetworkSpecifier() { + final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1); + final NetworkCapabilities nc1 = new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_WIFI) + .setNetworkSpecifier(specifier) + .build(); + assertEquals(specifier, nc1.getNetworkSpecifier()); + try { + final NetworkCapabilities nc2 = new NetworkCapabilities.Builder() + .setNetworkSpecifier(specifier) + .build(); + fail("Must have a single transport type. Without transport type or multiple transport" + + " types is invalid."); + } catch (IllegalStateException expected) { } + } + + @Test + public void testWifiAwareNetworkSpecifier() { + final NetworkCapabilities nc = new NetworkCapabilities() + .addTransportType(TRANSPORT_WIFI_AWARE); + // If NetworkSpecifier is not set, the default value is null. + assertNull(nc.getNetworkSpecifier()); + final WifiAwareNetworkSpecifier specifier = new WifiAwareNetworkSpecifier.Builder( + mDiscoverySession, mPeerHandle).build(); + nc.setNetworkSpecifier(specifier); + assertEquals(specifier, nc.getNetworkSpecifier()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testAdministratorUidsAndOwnerUid() { + // Test default owner uid. + // If the owner uid is not set, the default value should be Process.INVALID_UID. + final NetworkCapabilities nc1 = new NetworkCapabilities.Builder().build(); + assertEquals(INVALID_UID, nc1.getOwnerUid()); + // Test setAdministratorUids and getAdministratorUids. + final int[] administratorUids = {1001, 10001}; + final NetworkCapabilities nc2 = new NetworkCapabilities.Builder() + .setAdministratorUids(administratorUids) + .build(); + assertTrue(Arrays.equals(administratorUids, nc2.getAdministratorUids())); + // Test setOwnerUid and getOwnerUid. + // The owner UID must be included in administrator UIDs, or throw IllegalStateException. + try { + final NetworkCapabilities nc3 = new NetworkCapabilities.Builder() + .setOwnerUid(1001) + .build(); + fail("The owner UID must be included in administrator UIDs."); + } catch (IllegalStateException expected) { } + final NetworkCapabilities nc4 = new NetworkCapabilities.Builder() + .setAdministratorUids(administratorUids) + .setOwnerUid(1001) + .build(); + assertEquals(1001, nc4.getOwnerUid()); + try { + final NetworkCapabilities nc5 = new NetworkCapabilities.Builder() + .setAdministratorUids(null) + .build(); + fail("Should not set null into setAdministratorUids"); + } catch (NullPointerException expected) { } + } + + private static NetworkCapabilities capsWithSubIds(Integer ... subIds) { + // Since the NetworkRequest would put NOT_VCN_MANAGED capabilities in general, for + // every NetworkCapabilities that simulates networks needs to add it too in order to + // satisfy these requests. + final NetworkCapabilities nc = new NetworkCapabilities.Builder() + .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED) + .setSubscriptionIds(new ArraySet<>(subIds)).build(); + assertEquals(new ArraySet<>(subIds), nc.getSubscriptionIds()); + return nc; + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + public void testSubIds() throws Exception { + final NetworkCapabilities ncWithoutId = capsWithSubIds(); + final NetworkCapabilities ncWithId = capsWithSubIds(TEST_SUBID1); + final NetworkCapabilities ncWithOtherIds = capsWithSubIds(TEST_SUBID1, TEST_SUBID3); + final NetworkCapabilities ncWithoutRequestedIds = capsWithSubIds(TEST_SUBID3); + + final NetworkRequest requestWithoutId = new NetworkRequest.Builder().build(); + assertEmpty(requestWithoutId.networkCapabilities.getSubscriptionIds()); + final NetworkRequest requestWithIds = new NetworkRequest.Builder() + .setSubscriptionIds(Set.of(TEST_SUBID1, TEST_SUBID2)).build(); + assertEquals(Set.of(TEST_SUBID1, TEST_SUBID2), + requestWithIds.networkCapabilities.getSubscriptionIds()); + + assertFalse(requestWithIds.canBeSatisfiedBy(ncWithoutId)); + assertTrue(requestWithIds.canBeSatisfiedBy(ncWithOtherIds)); + assertFalse(requestWithIds.canBeSatisfiedBy(ncWithoutRequestedIds)); + assertTrue(requestWithIds.canBeSatisfiedBy(ncWithId)); + assertTrue(requestWithoutId.canBeSatisfiedBy(ncWithoutId)); + assertTrue(requestWithoutId.canBeSatisfiedBy(ncWithId)); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + public void testEqualsSubIds() throws Exception { + assertEquals(capsWithSubIds(), capsWithSubIds()); + assertNotEquals(capsWithSubIds(), capsWithSubIds(TEST_SUBID1)); + assertEquals(capsWithSubIds(TEST_SUBID1), capsWithSubIds(TEST_SUBID1)); + assertNotEquals(capsWithSubIds(TEST_SUBID1), capsWithSubIds(TEST_SUBID2)); + assertNotEquals(capsWithSubIds(TEST_SUBID1), capsWithSubIds(TEST_SUBID2, TEST_SUBID1)); + assertEquals(capsWithSubIds(TEST_SUBID1, TEST_SUBID2), + capsWithSubIds(TEST_SUBID2, TEST_SUBID1)); + } + + @Test + public void testLinkBandwidthKbps() { + final NetworkCapabilities nc = new NetworkCapabilities(); + // The default value of LinkDown/UpstreamBandwidthKbps should be LINK_BANDWIDTH_UNSPECIFIED. + assertEquals(LINK_BANDWIDTH_UNSPECIFIED, nc.getLinkDownstreamBandwidthKbps()); + assertEquals(LINK_BANDWIDTH_UNSPECIFIED, nc.getLinkUpstreamBandwidthKbps()); + nc.setLinkDownstreamBandwidthKbps(512); + nc.setLinkUpstreamBandwidthKbps(128); + assertEquals(512, nc.getLinkDownstreamBandwidthKbps()); + assertNotEquals(128, nc.getLinkDownstreamBandwidthKbps()); + assertEquals(128, nc.getLinkUpstreamBandwidthKbps()); + assertNotEquals(512, nc.getLinkUpstreamBandwidthKbps()); + } + + @Test + public void testSignalStrength() { + final NetworkCapabilities nc = new NetworkCapabilities(); + // The default value of signal strength should be SIGNAL_STRENGTH_UNSPECIFIED. + assertEquals(SIGNAL_STRENGTH_UNSPECIFIED, nc.getSignalStrength()); + nc.setSignalStrength(-80); + assertEquals(-80, nc.getSignalStrength()); + assertNotEquals(-50, nc.getSignalStrength()); + } + + private void assertNoTransport(NetworkCapabilities nc) { + for (int i = MIN_TRANSPORT; i <= MAX_TRANSPORT; i++) { + assertFalse(nc.hasTransport(i)); + } + } + + // Checks that all transport types from MIN_TRANSPORT to maxTransportType are set and all + // transport types from maxTransportType + 1 to MAX_TRANSPORT are not set when positiveSequence + // is true. If positiveSequence is false, then the check sequence is opposite. + private void checkCurrentTransportTypes(NetworkCapabilities nc, int maxTransportType, + boolean positiveSequence) { + for (int i = MIN_TRANSPORT; i <= maxTransportType; i++) { + if (positiveSequence) { + assertTrue(nc.hasTransport(i)); + } else { + assertFalse(nc.hasTransport(i)); + } + } + for (int i = MAX_TRANSPORT; i > maxTransportType; i--) { + if (positiveSequence) { + assertFalse(nc.hasTransport(i)); + } else { + assertTrue(nc.hasTransport(i)); + } + } + } + + @Test + public void testMultipleTransportTypes() { + final NetworkCapabilities nc = new NetworkCapabilities(); + assertNoTransport(nc); + // Test adding multiple transport types. + for (int i = MIN_TRANSPORT; i <= MAX_TRANSPORT; i++) { + nc.addTransportType(i); + checkCurrentTransportTypes(nc, i, true /* positiveSequence */); + } + // Test removing multiple transport types. + for (int i = MIN_TRANSPORT; i <= MAX_TRANSPORT; i++) { + nc.removeTransportType(i); + checkCurrentTransportTypes(nc, i, false /* positiveSequence */); + } + assertNoTransport(nc); + nc.addTransportType(TRANSPORT_WIFI); + assertTrue(nc.hasTransport(TRANSPORT_WIFI)); + assertFalse(nc.hasTransport(TRANSPORT_VPN)); + nc.addTransportType(TRANSPORT_VPN); + assertTrue(nc.hasTransport(TRANSPORT_WIFI)); + assertTrue(nc.hasTransport(TRANSPORT_VPN)); + nc.removeTransportType(TRANSPORT_WIFI); + assertFalse(nc.hasTransport(TRANSPORT_WIFI)); + assertTrue(nc.hasTransport(TRANSPORT_VPN)); + nc.removeTransportType(TRANSPORT_VPN); + assertFalse(nc.hasTransport(TRANSPORT_WIFI)); + assertFalse(nc.hasTransport(TRANSPORT_VPN)); + assertNoTransport(nc); + } + + @Test + public void testAddAndRemoveTransportType() { + final NetworkCapabilities nc = new NetworkCapabilities(); + try { + nc.addTransportType(-1); + fail("Should not set invalid transport type into addTransportType"); + } catch (IllegalArgumentException expected) { } + try { + nc.removeTransportType(-1); + fail("Should not set invalid transport type into removeTransportType"); + } catch (IllegalArgumentException e) { } + } + + /** + * Test TransportInfo to verify redaction mechanism. + */ + private static class TestTransportInfo implements TransportInfo { + public final boolean locationRedacted; + public final boolean localMacAddressRedacted; + public final boolean settingsRedacted; + + TestTransportInfo() { + locationRedacted = false; + localMacAddressRedacted = false; + settingsRedacted = false; + } + + TestTransportInfo(boolean locationRedacted, + boolean localMacAddressRedacted, + boolean settingsRedacted) { + this.locationRedacted = locationRedacted; + this.localMacAddressRedacted = + localMacAddressRedacted; + this.settingsRedacted = settingsRedacted; + } + + @Override + public TransportInfo makeCopy(@NetworkCapabilities.RedactionType long redactions) { + return new TestTransportInfo( + (redactions & NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION) != 0, + (redactions & REDACT_FOR_LOCAL_MAC_ADDRESS) != 0, + (redactions & REDACT_FOR_NETWORK_SETTINGS) != 0 + ); + } + + @Override + public @NetworkCapabilities.RedactionType long getApplicableRedactions() { + return REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_LOCAL_MAC_ADDRESS + | REDACT_FOR_NETWORK_SETTINGS; + } + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testBuilder() { + final int ownerUid = 1001; + final int signalStrength = -80; + final int requestUid = 10100; + final int[] administratorUids = {ownerUid, 10001}; + final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1); + final TransportInfo transportInfo = new TransportInfo() {}; + final String ssid = "TEST_SSID"; + final String packageName = "com.google.test.networkcapabilities"; + final NetworkCapabilities nc = new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_WIFI) + .addTransportType(TRANSPORT_CELLULAR) + .removeTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_EIMS) + .addCapability(NET_CAPABILITY_CBS) + .removeCapability(NET_CAPABILITY_CBS) + .setAdministratorUids(administratorUids) + .setOwnerUid(ownerUid) + .setLinkDownstreamBandwidthKbps(512) + .setLinkUpstreamBandwidthKbps(128) + .setNetworkSpecifier(specifier) + .setTransportInfo(transportInfo) + .setSignalStrength(signalStrength) + .setSsid(ssid) + .setRequestorUid(requestUid) + .setRequestorPackageName(packageName) + .build(); + assertEquals(1, nc.getTransportTypes().length); + assertEquals(TRANSPORT_WIFI, nc.getTransportTypes()[0]); + assertTrue(nc.hasCapability(NET_CAPABILITY_EIMS)); + assertFalse(nc.hasCapability(NET_CAPABILITY_CBS)); + assertTrue(Arrays.equals(administratorUids, nc.getAdministratorUids())); + assertEquals(ownerUid, nc.getOwnerUid()); + assertEquals(512, nc.getLinkDownstreamBandwidthKbps()); + assertNotEquals(128, nc.getLinkDownstreamBandwidthKbps()); + assertEquals(128, nc.getLinkUpstreamBandwidthKbps()); + assertNotEquals(512, nc.getLinkUpstreamBandwidthKbps()); + assertEquals(specifier, nc.getNetworkSpecifier()); + assertEquals(transportInfo, nc.getTransportInfo()); + assertEquals(signalStrength, nc.getSignalStrength()); + assertNotEquals(-50, nc.getSignalStrength()); + assertEquals(ssid, nc.getSsid()); + assertEquals(requestUid, nc.getRequestorUid()); + assertEquals(packageName, nc.getRequestorPackageName()); + // Cannot assign null into NetworkCapabilities.Builder + try { + final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder(null); + fail("Should not set null into NetworkCapabilities.Builder"); + } catch (NullPointerException expected) { } + assertEquals(nc, new NetworkCapabilities.Builder(nc).build()); + + if (isAtLeastS()) { + final NetworkCapabilities nc2 = new NetworkCapabilities.Builder() + .setSubscriptionIds(Set.of(TEST_SUBID1)).build(); + assertEquals(Set.of(TEST_SUBID1), nc2.getSubscriptionIds()); + } + } +} diff --git a/tests/common/java/android/net/NetworkProviderTest.kt b/tests/common/java/android/net/NetworkProviderTest.kt new file mode 100644 index 0000000000..340e6f9631 --- /dev/null +++ b/tests/common/java/android/net/NetworkProviderTest.kt @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2020 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 + +import android.app.Instrumentation +import android.content.Context +import android.net.NetworkCapabilities.TRANSPORT_TEST +import android.net.NetworkProviderTest.TestNetworkCallback.CallbackEntry.OnUnavailable +import android.net.NetworkProviderTest.TestNetworkProvider.CallbackEntry.OnNetworkRequestWithdrawn +import android.net.NetworkProviderTest.TestNetworkProvider.CallbackEntry.OnNetworkRequested +import android.os.Build +import android.os.HandlerThread +import android.os.Looper +import androidx.test.InstrumentationRegistry +import com.android.net.module.util.ArrayTrackRecord +import com.android.testutils.CompatUtil +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.isDevSdkInRange +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.verifyNoMoreInteractions +import java.util.UUID +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +private const val DEFAULT_TIMEOUT_MS = 5000L +private val instrumentation: Instrumentation + get() = InstrumentationRegistry.getInstrumentation() +private val context: Context get() = InstrumentationRegistry.getContext() +private val PROVIDER_NAME = "NetworkProviderTest" + +@RunWith(DevSdkIgnoreRunner::class) +@IgnoreUpTo(Build.VERSION_CODES.Q) +class NetworkProviderTest { + private val mCm = context.getSystemService(ConnectivityManager::class.java) + private val mHandlerThread = HandlerThread("${javaClass.simpleName} handler thread") + + @Before + fun setUp() { + instrumentation.getUiAutomation().adoptShellPermissionIdentity() + mHandlerThread.start() + } + + @After + fun tearDown() { + mHandlerThread.quitSafely() + instrumentation.getUiAutomation().dropShellPermissionIdentity() + } + + private class TestNetworkProvider(context: Context, looper: Looper) : + NetworkProvider(context, looper, PROVIDER_NAME) { + private val seenEvents = ArrayTrackRecord().newReadHead() + + sealed class CallbackEntry { + data class OnNetworkRequested( + val request: NetworkRequest, + val score: Int, + val id: Int + ) : CallbackEntry() + data class OnNetworkRequestWithdrawn(val request: NetworkRequest) : CallbackEntry() + } + + override fun onNetworkRequested(request: NetworkRequest, score: Int, id: Int) { + seenEvents.add(OnNetworkRequested(request, score, id)) + } + + override fun onNetworkRequestWithdrawn(request: NetworkRequest) { + seenEvents.add(OnNetworkRequestWithdrawn(request)) + } + + inline fun expectCallback( + crossinline predicate: (T) -> Boolean + ) = seenEvents.poll(DEFAULT_TIMEOUT_MS) { it is T && predicate(it) } + } + + private fun createNetworkProvider(ctx: Context = context): TestNetworkProvider { + return TestNetworkProvider(ctx, mHandlerThread.looper) + } + + @Test + fun testOnNetworkRequested() { + val provider = createNetworkProvider() + assertEquals(provider.getProviderId(), NetworkProvider.ID_NONE) + mCm.registerNetworkProvider(provider) + assertNotEquals(provider.getProviderId(), NetworkProvider.ID_NONE) + + val specifier = CompatUtil.makeTestNetworkSpecifier( + UUID.randomUUID().toString()) + val nr: NetworkRequest = NetworkRequest.Builder() + .addTransportType(TRANSPORT_TEST) + .setNetworkSpecifier(specifier) + .build() + val cb = ConnectivityManager.NetworkCallback() + mCm.requestNetwork(nr, cb) + provider.expectCallback() { callback -> + callback.request.getNetworkSpecifier() == specifier && + callback.request.hasTransport(TRANSPORT_TEST) + } + + val initialScore = 40 + val updatedScore = 60 + val nc = NetworkCapabilities().apply { + addTransportType(NetworkCapabilities.TRANSPORT_TEST) + removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED) + removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) + addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING) + addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) + setNetworkSpecifier(specifier) + } + val lp = LinkProperties() + val config = NetworkAgentConfig.Builder().build() + val agent = object : NetworkAgent(context, mHandlerThread.looper, "TestAgent", nc, lp, + initialScore, config, provider) {} + + provider.expectCallback() { callback -> + callback.request.getNetworkSpecifier() == specifier && + callback.score == initialScore && + callback.id == agent.providerId + } + + agent.sendNetworkScore(updatedScore) + provider.expectCallback() { callback -> + callback.request.getNetworkSpecifier() == specifier && + callback.score == updatedScore && + callback.id == agent.providerId + } + + mCm.unregisterNetworkCallback(cb) + provider.expectCallback() { callback -> + callback.request.getNetworkSpecifier() == specifier && + callback.request.hasTransport(TRANSPORT_TEST) + } + mCm.unregisterNetworkProvider(provider) + // Provider id should be ID_NONE after unregister network provider + assertEquals(provider.getProviderId(), NetworkProvider.ID_NONE) + // unregisterNetworkProvider should not crash even if it's called on an + // already unregistered provider. + mCm.unregisterNetworkProvider(provider) + } + + private class TestNetworkCallback : ConnectivityManager.NetworkCallback() { + private val seenEvents = ArrayTrackRecord().newReadHead() + sealed class CallbackEntry { + object OnUnavailable : CallbackEntry() + } + + override fun onUnavailable() { + seenEvents.add(OnUnavailable) + } + + inline fun expectCallback( + crossinline predicate: (T) -> Boolean + ) = seenEvents.poll(DEFAULT_TIMEOUT_MS) { it is T && predicate(it) } + } + + @Test + fun testDeclareNetworkRequestUnfulfillable() { + val mockContext = mock(Context::class.java) + doReturn(mCm).`when`(mockContext).getSystemService(Context.CONNECTIVITY_SERVICE) + val provider = createNetworkProvider(mockContext) + // ConnectivityManager not required at creation time after R + if (!isDevSdkInRange(0, Build.VERSION_CODES.R)) { + verifyNoMoreInteractions(mockContext) + } + + mCm.registerNetworkProvider(provider) + + val specifier = CompatUtil.makeTestNetworkSpecifier( + UUID.randomUUID().toString()) + val nr: NetworkRequest = NetworkRequest.Builder() + .addTransportType(TRANSPORT_TEST) + .setNetworkSpecifier(specifier) + .build() + + val cb = TestNetworkCallback() + mCm.requestNetwork(nr, cb) + provider.declareNetworkRequestUnfulfillable(nr) + cb.expectCallback() { nr.getNetworkSpecifier() == specifier } + mCm.unregisterNetworkProvider(provider) + } +} \ No newline at end of file diff --git a/tests/common/java/android/net/NetworkSpecifierTest.kt b/tests/common/java/android/net/NetworkSpecifierTest.kt new file mode 100644 index 0000000000..f3409f5359 --- /dev/null +++ b/tests/common/java/android/net/NetworkSpecifierTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2020 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 + +import android.os.Build +import androidx.test.filters.SmallTest +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import com.android.testutils.DevSdkIgnoreRunner +import kotlin.test.assertTrue +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(DevSdkIgnoreRunner::class) +@IgnoreUpTo(Build.VERSION_CODES.Q) +class NetworkSpecifierTest { + private class TestNetworkSpecifier( + val intData: Int = 123, + val stringData: String = "init" + ) : NetworkSpecifier() { + override fun canBeSatisfiedBy(other: NetworkSpecifier?): Boolean = + other != null && + other is TestNetworkSpecifier && + other.intData >= intData && + stringData.equals(other.stringData) + + override fun redact(): NetworkSpecifier = TestNetworkSpecifier(intData, "redact") + } + + @Test + fun testRedact() { + val ns: TestNetworkSpecifier = TestNetworkSpecifier() + val redactNs = ns.redact() + assertTrue(redactNs is TestNetworkSpecifier) + assertEquals(ns.intData, redactNs.intData) + assertNotEquals(ns.stringData, redactNs.stringData) + assertTrue("redact".equals(redactNs.stringData)) + } + + @Test + fun testcanBeSatisfiedBy() { + val target: TestNetworkSpecifier = TestNetworkSpecifier() + assertFalse(target.canBeSatisfiedBy(null)) + assertTrue(target.canBeSatisfiedBy(TestNetworkSpecifier())) + val otherNs = TelephonyNetworkSpecifier.Builder().setSubscriptionId(123).build() + assertFalse(target.canBeSatisfiedBy(otherNs)) + assertTrue(target.canBeSatisfiedBy(TestNetworkSpecifier(intData = 999))) + assertFalse(target.canBeSatisfiedBy(TestNetworkSpecifier(intData = 1))) + assertFalse(target.canBeSatisfiedBy(TestNetworkSpecifier(stringData = "diff"))) + } +} \ No newline at end of file diff --git a/tests/common/java/android/net/NetworkStackTest.java b/tests/common/java/android/net/NetworkStackTest.java new file mode 100644 index 0000000000..f8f9c72374 --- /dev/null +++ b/tests/common/java/android/net/NetworkStackTest.java @@ -0,0 +1,51 @@ +/* + * 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; + +import static org.junit.Assert.assertEquals; + +import android.os.Build; +import android.os.IBinder; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class NetworkStackTest { + @Rule + public DevSdkIgnoreRule mDevSdkIgnoreRule = new DevSdkIgnoreRule(); + + @Mock private IBinder mConnectorBinder; + + @Before public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testGetService() { + NetworkStack.setServiceForTest(mConnectorBinder); + assertEquals(NetworkStack.getService(), mConnectorBinder); + } +} diff --git a/tests/common/java/android/net/NetworkStateSnapshotTest.kt b/tests/common/java/android/net/NetworkStateSnapshotTest.kt new file mode 100644 index 0000000000..0ca4d9551f --- /dev/null +++ b/tests/common/java/android/net/NetworkStateSnapshotTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2021 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 + +import android.net.ConnectivityManager.TYPE_NONE +import android.net.ConnectivityManager.TYPE_WIFI +import android.net.InetAddresses.parseNumericAddress +import android.net.NetworkCapabilities.TRANSPORT_WIFI +import android.os.Build +import androidx.test.filters.SmallTest +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.assertParcelSane +import org.junit.Test +import org.junit.runner.RunWith +import java.net.Inet4Address +import java.net.Inet6Address + +private const val TEST_IMSI = "imsi1" +private const val TEST_SSID = "SSID1" +private const val TEST_NETID = 123 + +private val TEST_IPV4_GATEWAY = parseNumericAddress("192.168.222.3") as Inet4Address +private val TEST_IPV6_GATEWAY = parseNumericAddress("2001:db8::1") as Inet6Address +private val TEST_IPV4_LINKADDR = LinkAddress("192.168.222.123/24") +private val TEST_IPV6_LINKADDR = LinkAddress("2001:db8::123/64") +private val TEST_IFACE = "fake0" +private val TEST_LINK_PROPERTIES = LinkProperties().apply { + interfaceName = TEST_IFACE + addLinkAddress(TEST_IPV4_LINKADDR) + addLinkAddress(TEST_IPV6_LINKADDR) + + // Add default routes + addRoute(RouteInfo(IpPrefix(parseNumericAddress("0.0.0.0"), 0), TEST_IPV4_GATEWAY)) + addRoute(RouteInfo(IpPrefix(parseNumericAddress("::"), 0), TEST_IPV6_GATEWAY)) +} + +private val TEST_CAPABILITIES = NetworkCapabilities().apply { + addTransportType(TRANSPORT_WIFI) + setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, false) + setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, true) + setSSID(TEST_SSID) +} + +@SmallTest +@RunWith(DevSdkIgnoreRunner::class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) +class NetworkStateSnapshotTest { + + @Test + fun testParcelUnparcel() { + val emptySnapshot = NetworkStateSnapshot(Network(TEST_NETID), NetworkCapabilities(), + LinkProperties(), null, TYPE_NONE) + val snapshot = NetworkStateSnapshot( + Network(TEST_NETID), TEST_CAPABILITIES, TEST_LINK_PROPERTIES, TEST_IMSI, TYPE_WIFI) + assertParcelSane(emptySnapshot, 5) + assertParcelSane(snapshot, 5) + } +} diff --git a/tests/common/java/android/net/NetworkTest.java b/tests/common/java/android/net/NetworkTest.java new file mode 100644 index 0000000000..11d44b86bc --- /dev/null +++ b/tests/common/java/android/net/NetworkTest.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2015 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.platform.test.annotations.AppModeFull; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.net.DatagramSocket; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.SocketException; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkTest { + final Network mNetwork = new Network(99); + + @Test + public void testBindSocketOfInvalidFdThrows() throws Exception { + + final FileDescriptor fd = new FileDescriptor(); + assertFalse(fd.valid()); + + try { + mNetwork.bindSocket(fd); + fail("SocketException not thrown"); + } catch (SocketException expected) {} + } + + @Test + public void testBindSocketOfNonSocketFdThrows() throws Exception { + final File devNull = new File("/dev/null"); + assertTrue(devNull.canRead()); + + final FileInputStream fis = new FileInputStream(devNull); + assertTrue(null != fis.getFD()); + assertTrue(fis.getFD().valid()); + + try { + mNetwork.bindSocket(fis.getFD()); + fail("SocketException not thrown"); + } catch (SocketException expected) {} + } + + @Test + @AppModeFull(reason = "Socket cannot bind in instant app mode") + public void testBindSocketOfConnectedDatagramSocketThrows() throws Exception { + final DatagramSocket mDgramSocket = new DatagramSocket(0, (InetAddress) Inet6Address.ANY); + mDgramSocket.connect((InetAddress) Inet6Address.LOOPBACK, 53); + assertTrue(mDgramSocket.isConnected()); + + try { + mNetwork.bindSocket(mDgramSocket); + fail("SocketException not thrown"); + } catch (SocketException expected) {} + } + + @Test + public void testBindSocketOfLocalSocketThrows() throws Exception { + final LocalSocket mLocalClient = new LocalSocket(); + mLocalClient.bind(new LocalSocketAddress("testClient")); + assertTrue(mLocalClient.getFileDescriptor().valid()); + + try { + mNetwork.bindSocket(mLocalClient.getFileDescriptor()); + fail("SocketException not thrown"); + } catch (SocketException expected) {} + + final LocalServerSocket mLocalServer = new LocalServerSocket("testServer"); + mLocalClient.connect(mLocalServer.getLocalSocketAddress()); + assertTrue(mLocalClient.isConnected()); + + try { + mNetwork.bindSocket(mLocalClient.getFileDescriptor()); + fail("SocketException not thrown"); + } catch (SocketException expected) {} + } + + @Test + public void testZeroIsObviousForDebugging() { + Network zero = new Network(0); + assertEquals(0, zero.hashCode()); + assertEquals(0, zero.getNetworkHandle()); + assertEquals("0", zero.toString()); + } + + @Test + public void testGetNetworkHandle() { + Network one = new Network(1); + Network two = new Network(2); + Network three = new Network(3); + + // None of the hashcodes are zero. + assertNotEquals(0, one.hashCode()); + assertNotEquals(0, two.hashCode()); + assertNotEquals(0, three.hashCode()); + + // All the hashcodes are distinct. + assertNotEquals(one.hashCode(), two.hashCode()); + assertNotEquals(one.hashCode(), three.hashCode()); + assertNotEquals(two.hashCode(), three.hashCode()); + + // None of the handles are zero. + assertNotEquals(0, one.getNetworkHandle()); + assertNotEquals(0, two.getNetworkHandle()); + assertNotEquals(0, three.getNetworkHandle()); + + // All the handles are distinct. + assertNotEquals(one.getNetworkHandle(), two.getNetworkHandle()); + assertNotEquals(one.getNetworkHandle(), three.getNetworkHandle()); + assertNotEquals(two.getNetworkHandle(), three.getNetworkHandle()); + + // The handles are not equal to the hashcodes. + assertNotEquals(one.hashCode(), one.getNetworkHandle()); + assertNotEquals(two.hashCode(), two.getNetworkHandle()); + assertNotEquals(three.hashCode(), three.getNetworkHandle()); + + // Adjust as necessary to test an implementation's specific constants. + // When running with runtest, "adb logcat -s TestRunner" can be useful. + assertEquals(7700664333L, one.getNetworkHandle()); + assertEquals(11995631629L, two.getNetworkHandle()); + assertEquals(16290598925L, three.getNetworkHandle()); + } + + @Test + public void testGetPrivateDnsBypassingCopy() { + final Network copy = mNetwork.getPrivateDnsBypassingCopy(); + assertEquals(mNetwork.netId, copy.netId); + assertNotEquals(copy.netId, copy.getNetIdForResolv()); + assertNotEquals(mNetwork.getNetIdForResolv(), copy.getNetIdForResolv()); + } +} diff --git a/tests/common/java/android/net/OemNetworkPreferencesTest.java b/tests/common/java/android/net/OemNetworkPreferencesTest.java new file mode 100644 index 0000000000..fd29a9539d --- /dev/null +++ b/tests/common/java/android/net/OemNetworkPreferencesTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2021 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; + +import static com.android.testutils.MiscAsserts.assertThrows; +import static com.android.testutils.ParcelUtils.assertParcelSane; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.os.Build; + +import androidx.test.filters.SmallTest; + +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; +import com.android.testutils.DevSdkIgnoreRunner; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Map; + +@IgnoreUpTo(Build.VERSION_CODES.R) +@RunWith(DevSdkIgnoreRunner.class) +@SmallTest +public class OemNetworkPreferencesTest { + + private static final int TEST_PREF = OemNetworkPreferences.OEM_NETWORK_PREFERENCE_UNINITIALIZED; + private static final String TEST_PACKAGE = "com.google.apps.contacts"; + + private final OemNetworkPreferences.Builder mBuilder = new OemNetworkPreferences.Builder(); + + @Test + public void testBuilderAddNetworkPreferenceRequiresNonNullPackageName() { + assertThrows(NullPointerException.class, + () -> mBuilder.addNetworkPreference(null, TEST_PREF)); + } + + @Test + public void testBuilderRemoveNetworkPreferenceRequiresNonNullPackageName() { + assertThrows(NullPointerException.class, + () -> mBuilder.clearNetworkPreference(null)); + } + + @Test + public void testGetNetworkPreferenceReturnsCorrectValue() { + final int expectedNumberOfMappings = 1; + mBuilder.addNetworkPreference(TEST_PACKAGE, TEST_PREF); + + final Map networkPreferences = + mBuilder.build().getNetworkPreferences(); + + assertEquals(expectedNumberOfMappings, networkPreferences.size()); + assertTrue(networkPreferences.containsKey(TEST_PACKAGE)); + } + + @Test + public void testGetNetworkPreferenceReturnsUnmodifiableValue() { + final String newPackage = "new.com.google.apps.contacts"; + mBuilder.addNetworkPreference(TEST_PACKAGE, TEST_PREF); + + final Map networkPreferences = + mBuilder.build().getNetworkPreferences(); + + assertThrows(UnsupportedOperationException.class, + () -> networkPreferences.put(newPackage, TEST_PREF)); + + assertThrows(UnsupportedOperationException.class, + () -> networkPreferences.remove(TEST_PACKAGE)); + + } + + @Test + public void testToStringReturnsCorrectValue() { + mBuilder.addNetworkPreference(TEST_PACKAGE, TEST_PREF); + + final String networkPreferencesString = mBuilder.build().getNetworkPreferences().toString(); + + assertTrue(networkPreferencesString.contains(Integer.toString(TEST_PREF))); + assertTrue(networkPreferencesString.contains(TEST_PACKAGE)); + } + + @Test + public void testOemNetworkPreferencesParcelable() { + mBuilder.addNetworkPreference(TEST_PACKAGE, TEST_PREF); + + final OemNetworkPreferences prefs = mBuilder.build(); + + assertParcelSane(prefs, 1 /* fieldCount */); + } + + @Test + public void testAddNetworkPreferenceOverwritesPriorPreference() { + final int newPref = OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID; + mBuilder.addNetworkPreference(TEST_PACKAGE, TEST_PREF); + Map networkPreferences = + mBuilder.build().getNetworkPreferences(); + + assertTrue(networkPreferences.containsKey(TEST_PACKAGE)); + assertEquals(networkPreferences.get(TEST_PACKAGE).intValue(), TEST_PREF); + + mBuilder.addNetworkPreference(TEST_PACKAGE, newPref); + networkPreferences = mBuilder.build().getNetworkPreferences(); + + assertTrue(networkPreferences.containsKey(TEST_PACKAGE)); + assertEquals(networkPreferences.get(TEST_PACKAGE).intValue(), newPref); + } + + @Test + public void testRemoveNetworkPreferenceRemovesValue() { + mBuilder.addNetworkPreference(TEST_PACKAGE, TEST_PREF); + Map networkPreferences = + mBuilder.build().getNetworkPreferences(); + + assertTrue(networkPreferences.containsKey(TEST_PACKAGE)); + + mBuilder.clearNetworkPreference(TEST_PACKAGE); + networkPreferences = mBuilder.build().getNetworkPreferences(); + + assertFalse(networkPreferences.containsKey(TEST_PACKAGE)); + } + + @Test + public void testConstructorByOemNetworkPreferencesSetsValue() { + mBuilder.addNetworkPreference(TEST_PACKAGE, TEST_PREF); + OemNetworkPreferences networkPreference = mBuilder.build(); + + final Map networkPreferences = + new OemNetworkPreferences + .Builder(networkPreference) + .build() + .getNetworkPreferences(); + + assertTrue(networkPreferences.containsKey(TEST_PACKAGE)); + assertEquals(networkPreferences.get(TEST_PACKAGE).intValue(), TEST_PREF); + } +} diff --git a/tests/common/java/android/net/RouteInfoTest.java b/tests/common/java/android/net/RouteInfoTest.java new file mode 100644 index 0000000000..71689f9197 --- /dev/null +++ b/tests/common/java/android/net/RouteInfoTest.java @@ -0,0 +1,434 @@ +/* + * Copyright (C) 2010 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; + +import static android.net.RouteInfo.RTN_UNREACHABLE; + +import static com.android.testutils.MiscAsserts.assertEqualBothWays; +import static com.android.testutils.MiscAsserts.assertFieldCountEquals; +import static com.android.testutils.MiscAsserts.assertNotEqualEitherWay; +import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.os.Build; + +import androidx.core.os.BuildCompat; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class RouteInfoTest { + @Rule + public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(); + + private static final int INVALID_ROUTE_TYPE = -1; + + private InetAddress Address(String addr) { + return InetAddresses.parseNumericAddress(addr); + } + + private IpPrefix Prefix(String prefix) { + return new IpPrefix(prefix); + } + + private static boolean isAtLeastR() { + // BuildCompat.isAtLeastR is documented to return false on release SDKs (including R) + return Build.VERSION.SDK_INT > Build.VERSION_CODES.Q || BuildCompat.isAtLeastR(); + } + + @Test + public void testConstructor() { + RouteInfo r; + // Invalid input. + try { + r = new RouteInfo((IpPrefix) null, null, "rmnet0"); + fail("Expected RuntimeException: destination and gateway null"); + } catch (RuntimeException e) { } + + try { + r = new RouteInfo(Prefix("2001:db8:ace::/49"), Address("2001:db8::1"), "rmnet0", + INVALID_ROUTE_TYPE); + fail("Invalid route type should cause exception"); + } catch (IllegalArgumentException e) { } + + try { + r = new RouteInfo(Prefix("2001:db8:ace::/49"), Address("192.0.2.1"), "rmnet0", + RTN_UNREACHABLE); + fail("Address family mismatch should cause exception"); + } catch (IllegalArgumentException e) { } + + try { + r = new RouteInfo(Prefix("0.0.0.0/0"), Address("2001:db8::1"), "rmnet0", + RTN_UNREACHABLE); + fail("Address family mismatch should cause exception"); + } catch (IllegalArgumentException e) { } + + // Null destination is default route. + r = new RouteInfo((IpPrefix) null, Address("2001:db8::1"), null); + assertEquals(Prefix("::/0"), r.getDestination()); + assertEquals(Address("2001:db8::1"), r.getGateway()); + assertNull(r.getInterface()); + + r = new RouteInfo((IpPrefix) null, Address("192.0.2.1"), "wlan0"); + assertEquals(Prefix("0.0.0.0/0"), r.getDestination()); + assertEquals(Address("192.0.2.1"), r.getGateway()); + assertEquals("wlan0", r.getInterface()); + + // Null gateway sets gateway to unspecified address (why?). + r = new RouteInfo(Prefix("2001:db8:beef:cafe::/48"), null, "lo"); + assertEquals(Prefix("2001:db8:beef::/48"), r.getDestination()); + assertEquals(Address("::"), r.getGateway()); + assertEquals("lo", r.getInterface()); + + r = new RouteInfo(Prefix("192.0.2.5/24"), null); + assertEquals(Prefix("192.0.2.0/24"), r.getDestination()); + assertEquals(Address("0.0.0.0"), r.getGateway()); + assertNull(r.getInterface()); + } + + @Test + public void testMatches() { + class PatchedRouteInfo { + private final RouteInfo mRouteInfo; + + public PatchedRouteInfo(IpPrefix destination, InetAddress gateway, String iface) { + mRouteInfo = new RouteInfo(destination, gateway, iface); + } + + public boolean matches(InetAddress destination) { + return mRouteInfo.matches(destination); + } + } + + PatchedRouteInfo r; + + r = new PatchedRouteInfo(Prefix("2001:db8:f00::ace:d00d/127"), null, "rmnet0"); + assertTrue(r.matches(Address("2001:db8:f00::ace:d00c"))); + assertTrue(r.matches(Address("2001:db8:f00::ace:d00d"))); + assertFalse(r.matches(Address("2001:db8:f00::ace:d00e"))); + assertFalse(r.matches(Address("2001:db8:f00::bad:d00d"))); + assertFalse(r.matches(Address("2001:4868:4860::8888"))); + assertFalse(r.matches(Address("8.8.8.8"))); + + r = new PatchedRouteInfo(Prefix("192.0.2.0/23"), null, "wlan0"); + assertTrue(r.matches(Address("192.0.2.43"))); + assertTrue(r.matches(Address("192.0.3.21"))); + assertFalse(r.matches(Address("192.0.0.21"))); + assertFalse(r.matches(Address("8.8.8.8"))); + + PatchedRouteInfo ipv6Default = new PatchedRouteInfo(Prefix("::/0"), null, "rmnet0"); + assertTrue(ipv6Default.matches(Address("2001:db8::f00"))); + assertFalse(ipv6Default.matches(Address("192.0.2.1"))); + + PatchedRouteInfo ipv4Default = new PatchedRouteInfo(Prefix("0.0.0.0/0"), null, "rmnet0"); + assertTrue(ipv4Default.matches(Address("255.255.255.255"))); + assertTrue(ipv4Default.matches(Address("192.0.2.1"))); + assertFalse(ipv4Default.matches(Address("2001:db8::f00"))); + } + + @Test + public void testEquals() { + // IPv4 + RouteInfo r1 = new RouteInfo(Prefix("2001:db8:ace::/48"), Address("2001:db8::1"), "wlan0"); + RouteInfo r2 = new RouteInfo(Prefix("2001:db8:ace::/48"), Address("2001:db8::1"), "wlan0"); + assertEqualBothWays(r1, r2); + + RouteInfo r3 = new RouteInfo(Prefix("2001:db8:ace::/49"), Address("2001:db8::1"), "wlan0"); + RouteInfo r4 = new RouteInfo(Prefix("2001:db8:ace::/48"), Address("2001:db8::2"), "wlan0"); + RouteInfo r5 = new RouteInfo(Prefix("2001:db8:ace::/48"), Address("2001:db8::1"), "rmnet0"); + assertNotEqualEitherWay(r1, r3); + assertNotEqualEitherWay(r1, r4); + assertNotEqualEitherWay(r1, r5); + + // IPv6 + r1 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.1"), "wlan0"); + r2 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.1"), "wlan0"); + assertEqualBothWays(r1, r2); + + r3 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0"); + r4 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.2"), "wlan0"); + r5 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.1"), "rmnet0"); + assertNotEqualEitherWay(r1, r3); + assertNotEqualEitherWay(r1, r4); + assertNotEqualEitherWay(r1, r5); + + // Interfaces (but not destinations or gateways) can be null. + r1 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.1"), null); + r2 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.1"), null); + r3 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0"); + assertEqualBothWays(r1, r2); + assertNotEqualEitherWay(r1, r3); + } + + @Test + public void testHostAndDefaultRoutes() { + RouteInfo r; + + r = new RouteInfo(Prefix("0.0.0.0/0"), Address("0.0.0.0"), "wlan0"); + assertFalse(r.isHostRoute()); + assertTrue(r.isDefaultRoute()); + assertTrue(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(Prefix("::/0"), Address("::"), "wlan0"); + assertFalse(r.isHostRoute()); + assertTrue(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertTrue(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(Prefix("192.0.2.0/24"), null, "wlan0"); + assertFalse(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(Prefix("2001:db8::/48"), null, "wlan0"); + assertFalse(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(Prefix("192.0.2.0/32"), Address("0.0.0.0"), "wlan0"); + assertTrue(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(Prefix("2001:db8::/128"), Address("::"), "wlan0"); + assertTrue(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(Prefix("192.0.2.0/32"), null, "wlan0"); + assertTrue(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(Prefix("2001:db8::/128"), null, "wlan0"); + assertTrue(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(Prefix("::/128"), Address("fe80::"), "wlan0"); + assertTrue(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(Prefix("0.0.0.0/32"), Address("192.0.2.1"), "wlan0"); + assertTrue(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(Prefix("0.0.0.0/32"), Address("192.0.2.1"), "wlan0"); + assertTrue(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), RTN_UNREACHABLE); + assertFalse(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertTrue(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE); + assertFalse(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertTrue(r.isIPv6UnreachableDefault()); + } + } + + @Test + public void testTruncation() { + LinkAddress l; + RouteInfo r; + + l = new LinkAddress("192.0.2.5/30"); + r = new RouteInfo(l, Address("192.0.2.1"), "wlan0"); + assertEquals("192.0.2.4", r.getDestination().getAddress().getHostAddress()); + + l = new LinkAddress("2001:db8:1:f::5/63"); + r = new RouteInfo(l, Address("2001:db8:5::1"), "wlan0"); + assertEquals("2001:db8:1:e::", r.getDestination().getAddress().getHostAddress()); + } + + // Make sure that creating routes to multicast addresses doesn't throw an exception. Even though + // there's nothing we can do with them, we don't want to crash if, e.g., someone calls + // requestRouteToHostAddress("230.0.0.0", MOBILE_HIPRI); + @Test + public void testMulticastRoute() { + RouteInfo r; + r = new RouteInfo(Prefix("230.0.0.0/32"), Address("192.0.2.1"), "wlan0"); + r = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::1"), "wlan0"); + // No exceptions? Good. + } + + @Test + public void testParceling() { + RouteInfo r; + r = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), null); + assertParcelingIsLossless(r); + r = new RouteInfo(Prefix("192.0.2.0/24"), null, "wlan0"); + assertParcelingIsLossless(r); + r = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0", RTN_UNREACHABLE); + assertParcelingIsLossless(r); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testMtuParceling() { + final RouteInfo r = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::"), "testiface", + RTN_UNREACHABLE, 1450 /* mtu */); + assertParcelingIsLossless(r); + } + + @Test @IgnoreAfter(Build.VERSION_CODES.Q) + public void testFieldCount_Q() { + assertFieldCountEquals(6, RouteInfo.class); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testFieldCount() { + // Make sure any new field is covered by the above parceling tests when changing this number + assertFieldCountEquals(7, RouteInfo.class); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testMtu() { + RouteInfo r; + r = new RouteInfo(Prefix("0.0.0.0/0"), Address("0.0.0.0"), "wlan0", + RouteInfo.RTN_UNICAST, 1500); + assertEquals(1500, r.getMtu()); + + r = new RouteInfo(Prefix("0.0.0.0/0"), Address("0.0.0.0"), "wlan0"); + assertEquals(0, r.getMtu()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testRouteKey() { + RouteInfo.RouteKey k1, k2; + // Only prefix, null gateway and null interface + k1 = new RouteInfo(Prefix("2001:db8::/128"), null).getRouteKey(); + k2 = new RouteInfo(Prefix("2001:db8::/128"), null).getRouteKey(); + assertEquals(k1, k2); + assertEquals(k1.hashCode(), k2.hashCode()); + + // With prefix, gateway and interface. Type and MTU does not affect RouteKey equality + k1 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0", + RTN_UNREACHABLE, 1450).getRouteKey(); + k2 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0", + RouteInfo.RTN_UNICAST, 1400).getRouteKey(); + assertEquals(k1, k2); + assertEquals(k1.hashCode(), k2.hashCode()); + + // Different scope IDs are ignored by the kernel, so we consider them equal here too. + k1 = new RouteInfo(Prefix("2001:db8::/64"), Address("fe80::1%1"), "wlan0").getRouteKey(); + k2 = new RouteInfo(Prefix("2001:db8::/64"), Address("fe80::1%2"), "wlan0").getRouteKey(); + assertEquals(k1, k2); + assertEquals(k1.hashCode(), k2.hashCode()); + + // Different prefix + k1 = new RouteInfo(Prefix("192.0.2.0/24"), null).getRouteKey(); + k2 = new RouteInfo(Prefix("192.0.3.0/24"), null).getRouteKey(); + assertNotEquals(k1, k2); + + // Different gateway + k1 = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::1"), null).getRouteKey(); + k2 = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::2"), null).getRouteKey(); + assertNotEquals(k1, k2); + + // Different interface + k1 = new RouteInfo(Prefix("ff02::1/128"), null, "tun0").getRouteKey(); + k2 = new RouteInfo(Prefix("ff02::1/128"), null, "tun1").getRouteKey(); + assertNotEquals(k1, k2); + } +} diff --git a/tests/common/java/android/net/StaticIpConfigurationTest.java b/tests/common/java/android/net/StaticIpConfigurationTest.java new file mode 100644 index 0000000000..b5f23bf19a --- /dev/null +++ b/tests/common/java/android/net/StaticIpConfigurationTest.java @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2014 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; + +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 android.os.Parcel; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class StaticIpConfigurationTest { + + private static final String ADDRSTR = "192.0.2.2/25"; + private static final LinkAddress ADDR = new LinkAddress(ADDRSTR); + private static final InetAddress GATEWAY = IpAddress("192.0.2.1"); + private static final InetAddress OFFLINKGATEWAY = IpAddress("192.0.2.129"); + private static final InetAddress DNS1 = IpAddress("8.8.8.8"); + private static final InetAddress DNS2 = IpAddress("8.8.4.4"); + private static final InetAddress DNS3 = IpAddress("4.2.2.2"); + private static final String IFACE = "eth0"; + private static final String FAKE_DOMAINS = "google.com"; + + private static InetAddress IpAddress(String addr) { + return InetAddress.parseNumericAddress(addr); + } + + private void checkEmpty(StaticIpConfiguration s) { + assertNull(s.ipAddress); + assertNull(s.gateway); + assertNull(s.domains); + assertEquals(0, s.dnsServers.size()); + } + + private StaticIpConfiguration makeTestObject() { + StaticIpConfiguration s = new StaticIpConfiguration(); + s.ipAddress = ADDR; + s.gateway = GATEWAY; + s.dnsServers.add(DNS1); + s.dnsServers.add(DNS2); + s.dnsServers.add(DNS3); + s.domains = FAKE_DOMAINS; + return s; + } + + @Test + public void testConstructor() { + StaticIpConfiguration s = new StaticIpConfiguration(); + checkEmpty(s); + } + + @Test + public void testCopyAndClear() { + StaticIpConfiguration empty = new StaticIpConfiguration((StaticIpConfiguration) null); + checkEmpty(empty); + + StaticIpConfiguration s1 = makeTestObject(); + StaticIpConfiguration s2 = new StaticIpConfiguration(s1); + assertEquals(s1, s2); + s2.clear(); + assertEquals(empty, s2); + } + + @Test + public void testHashCodeAndEquals() { + HashSet hashCodes = new HashSet(); + hashCodes.add(0); + + StaticIpConfiguration s = new StaticIpConfiguration(); + // Check that this hash code is nonzero and different from all the ones seen so far. + assertTrue(hashCodes.add(s.hashCode())); + + s.ipAddress = ADDR; + assertTrue(hashCodes.add(s.hashCode())); + + s.gateway = GATEWAY; + assertTrue(hashCodes.add(s.hashCode())); + + s.dnsServers.add(DNS1); + assertTrue(hashCodes.add(s.hashCode())); + + s.dnsServers.add(DNS2); + assertTrue(hashCodes.add(s.hashCode())); + + s.dnsServers.add(DNS3); + assertTrue(hashCodes.add(s.hashCode())); + + s.domains = "example.com"; + assertTrue(hashCodes.add(s.hashCode())); + + assertFalse(s.equals(null)); + assertEquals(s, s); + + StaticIpConfiguration s2 = new StaticIpConfiguration(s); + assertEquals(s, s2); + + s.ipAddress = new LinkAddress(DNS1, 32); + assertNotEquals(s, s2); + + s2 = new StaticIpConfiguration(s); + s.domains = "foo"; + assertNotEquals(s, s2); + + s2 = new StaticIpConfiguration(s); + s.gateway = DNS2; + assertNotEquals(s, s2); + + s2 = new StaticIpConfiguration(s); + s.dnsServers.add(DNS3); + assertNotEquals(s, s2); + } + + @Test + public void testToLinkProperties() { + LinkProperties expected = new LinkProperties(); + expected.setInterfaceName(IFACE); + + StaticIpConfiguration s = new StaticIpConfiguration(); + assertEquals(expected, s.toLinkProperties(IFACE)); + + final RouteInfo connectedRoute = new RouteInfo(new IpPrefix(ADDRSTR), null, IFACE); + s.ipAddress = ADDR; + expected.addLinkAddress(ADDR); + expected.addRoute(connectedRoute); + assertEquals(expected, s.toLinkProperties(IFACE)); + + s.gateway = GATEWAY; + RouteInfo defaultRoute = new RouteInfo(new IpPrefix("0.0.0.0/0"), GATEWAY, IFACE); + expected.addRoute(defaultRoute); + assertEquals(expected, s.toLinkProperties(IFACE)); + + s.gateway = OFFLINKGATEWAY; + expected.removeRoute(defaultRoute); + defaultRoute = new RouteInfo(new IpPrefix("0.0.0.0/0"), OFFLINKGATEWAY, IFACE); + expected.addRoute(defaultRoute); + + RouteInfo gatewayRoute = new RouteInfo(new IpPrefix("192.0.2.129/32"), null, IFACE); + expected.addRoute(gatewayRoute); + assertEquals(expected, s.toLinkProperties(IFACE)); + + s.dnsServers.add(DNS1); + expected.addDnsServer(DNS1); + assertEquals(expected, s.toLinkProperties(IFACE)); + + s.dnsServers.add(DNS2); + s.dnsServers.add(DNS3); + expected.addDnsServer(DNS2); + expected.addDnsServer(DNS3); + assertEquals(expected, s.toLinkProperties(IFACE)); + + s.domains = FAKE_DOMAINS; + expected.setDomains(FAKE_DOMAINS); + assertEquals(expected, s.toLinkProperties(IFACE)); + + s.gateway = null; + expected.removeRoute(defaultRoute); + expected.removeRoute(gatewayRoute); + assertEquals(expected, s.toLinkProperties(IFACE)); + + // Without knowing the IP address, we don't have a directly-connected route, so we can't + // tell if the gateway is off-link or not and we don't add a host route. This isn't a real + // configuration, but we should at least not crash. + s.gateway = OFFLINKGATEWAY; + s.ipAddress = null; + expected.removeLinkAddress(ADDR); + expected.removeRoute(connectedRoute); + expected.addRoute(defaultRoute); + assertEquals(expected, s.toLinkProperties(IFACE)); + } + + private StaticIpConfiguration passThroughParcel(StaticIpConfiguration s) { + Parcel p = Parcel.obtain(); + StaticIpConfiguration s2 = null; + try { + s.writeToParcel(p, 0); + p.setDataPosition(0); + s2 = StaticIpConfiguration.readFromParcel(p); + } finally { + p.recycle(); + } + assertNotNull(s2); + return s2; + } + + @Test + public void testParceling() { + StaticIpConfiguration s = makeTestObject(); + StaticIpConfiguration s2 = passThroughParcel(s); + assertEquals(s, s2); + } + + @Test + public void testBuilder() { + final ArrayList dnsServers = new ArrayList<>(); + dnsServers.add(DNS1); + + final StaticIpConfiguration s = new StaticIpConfiguration.Builder() + .setIpAddress(ADDR) + .setGateway(GATEWAY) + .setDomains(FAKE_DOMAINS) + .setDnsServers(dnsServers) + .build(); + + assertEquals(s.ipAddress, s.getIpAddress()); + assertEquals(ADDR, s.getIpAddress()); + assertEquals(s.gateway, s.getGateway()); + assertEquals(GATEWAY, s.getGateway()); + assertEquals(s.domains, s.getDomains()); + assertEquals(FAKE_DOMAINS, s.getDomains()); + assertTrue(s.dnsServers.equals(s.getDnsServers())); + assertEquals(1, s.getDnsServers().size()); + assertEquals(DNS1, s.getDnsServers().get(0)); + } + + @Test + public void testAddDnsServers() { + final StaticIpConfiguration s = new StaticIpConfiguration((StaticIpConfiguration) null); + checkEmpty(s); + + s.addDnsServer(DNS1); + assertEquals(1, s.getDnsServers().size()); + assertEquals(DNS1, s.getDnsServers().get(0)); + + s.addDnsServer(DNS2); + s.addDnsServer(DNS3); + assertEquals(3, s.getDnsServers().size()); + assertEquals(DNS2, s.getDnsServers().get(1)); + assertEquals(DNS3, s.getDnsServers().get(2)); + } + + @Test + public void testGetRoutes() { + final StaticIpConfiguration s = makeTestObject(); + final List routeInfoList = s.getRoutes(IFACE); + + assertEquals(2, routeInfoList.size()); + assertEquals(new RouteInfo(ADDR, (InetAddress) null, IFACE), routeInfoList.get(0)); + assertEquals(new RouteInfo((IpPrefix) null, GATEWAY, IFACE), routeInfoList.get(1)); + } +} diff --git a/tests/common/java/android/net/TcpKeepalivePacketDataTest.kt b/tests/common/java/android/net/TcpKeepalivePacketDataTest.kt new file mode 100644 index 0000000000..7a18bb08fa --- /dev/null +++ b/tests/common/java/android/net/TcpKeepalivePacketDataTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2020 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 + +import android.net.InetAddresses.parseNumericAddress +import android.os.Build +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.assertFieldCountEquals +import com.android.testutils.assertParcelSane +import org.junit.Test +import org.junit.runner.RunWith +import java.net.InetAddress +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +@RunWith(DevSdkIgnoreRunner::class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) // TcpKeepalivePacketData added to SDK in S +class TcpKeepalivePacketDataTest { + private fun makeData( + srcAddress: InetAddress = parseNumericAddress("192.0.2.123"), + srcPort: Int = 1234, + dstAddress: InetAddress = parseNumericAddress("192.0.2.231"), + dstPort: Int = 4321, + data: ByteArray = byteArrayOf(1, 2, 3), + tcpSeq: Int = 135, + tcpAck: Int = 246, + tcpWnd: Int = 1234, + tcpWndScale: Int = 2, + ipTos: Int = 0x12, + ipTtl: Int = 10 + ) = TcpKeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, data, tcpSeq, tcpAck, + tcpWnd, tcpWndScale, ipTos, ipTtl) + + @Test + fun testEquals() { + val data1 = makeData() + val data2 = makeData() + assertEquals(data1, data2) + assertEquals(data1.hashCode(), data2.hashCode()) + } + + @Test + fun testNotEquals() { + assertNotEquals(makeData(srcAddress = parseNumericAddress("192.0.2.124")), makeData()) + assertNotEquals(makeData(srcPort = 1235), makeData()) + assertNotEquals(makeData(dstAddress = parseNumericAddress("192.0.2.232")), makeData()) + assertNotEquals(makeData(dstPort = 4322), makeData()) + // .equals does not test .packet, as it should be generated from the other fields + assertNotEquals(makeData(tcpSeq = 136), makeData()) + assertNotEquals(makeData(tcpAck = 247), makeData()) + assertNotEquals(makeData(tcpWnd = 1235), makeData()) + assertNotEquals(makeData(tcpWndScale = 3), makeData()) + assertNotEquals(makeData(ipTos = 0x14), makeData()) + assertNotEquals(makeData(ipTtl = 11), makeData()) + + // Update above assertions if field is added + assertFieldCountEquals(5, KeepalivePacketData::class.java) + assertFieldCountEquals(6, TcpKeepalivePacketData::class.java) + } + + @Test + fun testParcelUnparcel() { + assertParcelSane(makeData(), fieldCount = 6) { a, b -> + // .equals() does not verify .packet + a == b && a.packet contentEquals b.packet + } + } + + @Test + fun testToString() { + val data = makeData() + val str = data.toString() + + assertTrue(str.contains(data.srcAddress.hostAddress)) + assertTrue(str.contains(data.srcPort.toString())) + assertTrue(str.contains(data.dstAddress.hostAddress)) + assertTrue(str.contains(data.dstPort.toString())) + // .packet not included in toString() + assertTrue(str.contains(data.getTcpSeq().toString())) + assertTrue(str.contains(data.getTcpAck().toString())) + assertTrue(str.contains(data.getTcpWindow().toString())) + assertTrue(str.contains(data.getTcpWindowScale().toString())) + assertTrue(str.contains(data.getIpTos().toString())) + assertTrue(str.contains(data.getIpTtl().toString())) + + // Update above assertions if field is added + assertFieldCountEquals(5, KeepalivePacketData::class.java) + assertFieldCountEquals(6, TcpKeepalivePacketData::class.java) + } +} \ No newline at end of file diff --git a/tests/common/java/android/net/UidRangeTest.java b/tests/common/java/android/net/UidRangeTest.java new file mode 100644 index 0000000000..1b1c95431d --- /dev/null +++ b/tests/common/java/android/net/UidRangeTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2016 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; + +import static android.os.UserHandle.MIN_SECONDARY_USER_ID; +import static android.os.UserHandle.SYSTEM; +import static android.os.UserHandle.USER_SYSTEM; +import static android.os.UserHandle.getUid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.os.Build; +import android.os.UserHandle; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class UidRangeTest { + + /* + * UidRange is no longer passed to netd. UID ranges between the framework and netd are passed as + * UidRangeParcel objects. + */ + + @Rule + public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(); + + @Test + public void testSingleItemUidRangeAllowed() { + new UidRange(123, 123); + new UidRange(0, 0); + new UidRange(Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + @Test + public void testNegativeUidsDisallowed() { + try { + new UidRange(-2, 100); + fail("Exception not thrown for negative start UID"); + } catch (IllegalArgumentException expected) { + } + + try { + new UidRange(-200, -100); + fail("Exception not thrown for negative stop UID"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testStopLessThanStartDisallowed() { + final int x = 4195000; + try { + new UidRange(x, x - 1); + fail("Exception not thrown for negative-length UID range"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testGetStartAndEndUser() throws Exception { + final UidRange uidRangeOfPrimaryUser = new UidRange( + getUid(USER_SYSTEM, 10000), getUid(USER_SYSTEM, 10100)); + final UidRange uidRangeOfSecondaryUser = new UidRange( + getUid(MIN_SECONDARY_USER_ID, 10000), getUid(MIN_SECONDARY_USER_ID, 10100)); + assertEquals(USER_SYSTEM, uidRangeOfPrimaryUser.getStartUser()); + assertEquals(USER_SYSTEM, uidRangeOfPrimaryUser.getEndUser()); + assertEquals(MIN_SECONDARY_USER_ID, uidRangeOfSecondaryUser.getStartUser()); + assertEquals(MIN_SECONDARY_USER_ID, uidRangeOfSecondaryUser.getEndUser()); + + final UidRange uidRangeForDifferentUsers = new UidRange( + getUid(USER_SYSTEM, 10000), getUid(MIN_SECONDARY_USER_ID, 10100)); + assertEquals(USER_SYSTEM, uidRangeOfPrimaryUser.getStartUser()); + assertEquals(MIN_SECONDARY_USER_ID, uidRangeOfSecondaryUser.getEndUser()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + public void testCreateForUser() throws Exception { + final UidRange uidRangeOfPrimaryUser = UidRange.createForUser(SYSTEM); + final UidRange uidRangeOfSecondaryUser = UidRange.createForUser( + UserHandle.of(USER_SYSTEM + 1)); + assertTrue(uidRangeOfPrimaryUser.stop < uidRangeOfSecondaryUser.start); + assertEquals(USER_SYSTEM, uidRangeOfPrimaryUser.getStartUser()); + assertEquals(USER_SYSTEM, uidRangeOfPrimaryUser.getEndUser()); + assertEquals(USER_SYSTEM + 1, uidRangeOfSecondaryUser.getStartUser()); + assertEquals(USER_SYSTEM + 1, uidRangeOfSecondaryUser.getEndUser()); + } +} diff --git a/tests/common/java/android/net/UnderlyingNetworkInfoTest.kt b/tests/common/java/android/net/UnderlyingNetworkInfoTest.kt new file mode 100644 index 0000000000..87cfb345e5 --- /dev/null +++ b/tests/common/java/android/net/UnderlyingNetworkInfoTest.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2021 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 + +import android.os.Build +import androidx.test.filters.SmallTest +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.assertParcelSane +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.test.assertEquals + +private const val TEST_OWNER_UID = 123 +private const val TEST_IFACE = "test_tun0" +private val TEST_IFACE_LIST = listOf("wlan0", "rmnet_data0", "eth0") + +@SmallTest +@RunWith(DevSdkIgnoreRunner::class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) +class UnderlyingNetworkInfoTest { + @Test + fun testParcelUnparcel() { + val testInfo = UnderlyingNetworkInfo(TEST_OWNER_UID, TEST_IFACE, TEST_IFACE_LIST) + assertEquals(TEST_OWNER_UID, testInfo.ownerUid) + assertEquals(TEST_IFACE, testInfo.iface) + assertEquals(TEST_IFACE_LIST, testInfo.underlyingIfaces) + assertParcelSane(testInfo, 3) + + val emptyInfo = UnderlyingNetworkInfo(0, String(), listOf()) + assertEquals(0, emptyInfo.ownerUid) + assertEquals(String(), emptyInfo.iface) + assertEquals(listOf(), emptyInfo.underlyingIfaces) + assertParcelSane(emptyInfo, 3) + } +} \ No newline at end of file diff --git a/tests/common/java/android/net/apf/ApfCapabilitiesTest.java b/tests/common/java/android/net/apf/ApfCapabilitiesTest.java new file mode 100644 index 0000000000..d50406fd3a --- /dev/null +++ b/tests/common/java/android/net/apf/ApfCapabilitiesTest.java @@ -0,0 +1,99 @@ +/* + * 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.apf; + +import static com.android.testutils.ParcelUtils.assertParcelSane; + +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 android.content.Context; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ApfCapabilitiesTest { + private Context mContext; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getContext(); + } + + @Test + public void testConstructAndParcel() { + final ApfCapabilities caps = new ApfCapabilities(123, 456, 789); + assertEquals(123, caps.apfVersionSupported); + assertEquals(456, caps.maximumApfProgramSize); + assertEquals(789, caps.apfPacketFormat); + + assertParcelSane(caps, 3); + } + + @Test + public void testEquals() { + assertEquals(new ApfCapabilities(1, 2, 3), new ApfCapabilities(1, 2, 3)); + assertNotEquals(new ApfCapabilities(2, 2, 3), new ApfCapabilities(1, 2, 3)); + assertNotEquals(new ApfCapabilities(1, 3, 3), new ApfCapabilities(1, 2, 3)); + assertNotEquals(new ApfCapabilities(1, 2, 4), new ApfCapabilities(1, 2, 3)); + } + + @Test + public void testHasDataAccess() { + //hasDataAccess is only supported starting at apf version 4. + ApfCapabilities caps = new ApfCapabilities(1 /* apfVersionSupported */, 2, 3); + assertFalse(caps.hasDataAccess()); + + caps = new ApfCapabilities(4 /* apfVersionSupported */, 5, 6); + assertTrue(caps.hasDataAccess()); + } + + @Test + public void testGetApfDrop8023Frames() { + // Get com.android.internal.R.bool.config_apfDrop802_3Frames. The test cannot directly + // use R.bool.config_apfDrop802_3Frames because that is not a stable resource ID. + final int resId = mContext.getResources().getIdentifier("config_apfDrop802_3Frames", + "bool", "android"); + final boolean shouldDrop8023Frames = mContext.getResources().getBoolean(resId); + final boolean actual = ApfCapabilities.getApfDrop8023Frames(); + assertEquals(shouldDrop8023Frames, actual); + } + + @Test + public void testGetApfEtherTypeBlackList() { + // Get com.android.internal.R.array.config_apfEthTypeBlackList. The test cannot directly + // use R.array.config_apfEthTypeBlackList because that is not a stable resource ID. + final int resId = mContext.getResources().getIdentifier("config_apfEthTypeBlackList", + "array", "android"); + final int[] blacklistedEtherTypeArray = mContext.getResources().getIntArray(resId); + final int[] actual = ApfCapabilities.getApfEtherTypeBlackList(); + assertNotNull(actual); + assertTrue(Arrays.equals(blacklistedEtherTypeArray, actual)); + } +} diff --git a/tests/common/java/android/net/metrics/ApfProgramEventTest.kt b/tests/common/java/android/net/metrics/ApfProgramEventTest.kt new file mode 100644 index 0000000000..0b7b74097c --- /dev/null +++ b/tests/common/java/android/net/metrics/ApfProgramEventTest.kt @@ -0,0 +1,72 @@ +/* + * 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.metrics; + +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.assertParcelSane +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class ApfProgramEventTest { + private infix fun Int.hasFlag(flag: Int) = (this and (1 shl flag)) != 0 + + @Test + fun testBuilderAndParcel() { + val apfProgramEvent = ApfProgramEvent.Builder() + .setLifetime(1) + .setActualLifetime(2) + .setFilteredRas(3) + .setCurrentRas(4) + .setProgramLength(5) + .setFlags(true, true) + .build() + + assertEquals(1, apfProgramEvent.lifetime) + assertEquals(2, apfProgramEvent.actualLifetime) + assertEquals(3, apfProgramEvent.filteredRas) + assertEquals(4, apfProgramEvent.currentRas) + assertEquals(5, apfProgramEvent.programLength) + assertEquals(ApfProgramEvent.flagsFor(true, true), apfProgramEvent.flags) + + assertParcelSane(apfProgramEvent, 6) + } + + @Test + fun testFlagsFor() { + var flags = ApfProgramEvent.flagsFor(false, false) + assertFalse(flags hasFlag ApfProgramEvent.FLAG_HAS_IPV4_ADDRESS) + assertFalse(flags hasFlag ApfProgramEvent.FLAG_MULTICAST_FILTER_ON) + + flags = ApfProgramEvent.flagsFor(true, false) + assertTrue(flags hasFlag ApfProgramEvent.FLAG_HAS_IPV4_ADDRESS) + assertFalse(flags hasFlag ApfProgramEvent.FLAG_MULTICAST_FILTER_ON) + + flags = ApfProgramEvent.flagsFor(false, true) + assertFalse(flags hasFlag ApfProgramEvent.FLAG_HAS_IPV4_ADDRESS) + assertTrue(flags hasFlag ApfProgramEvent.FLAG_MULTICAST_FILTER_ON) + + flags = ApfProgramEvent.flagsFor(true, true) + assertTrue(flags hasFlag ApfProgramEvent.FLAG_HAS_IPV4_ADDRESS) + assertTrue(flags hasFlag ApfProgramEvent.FLAG_MULTICAST_FILTER_ON) + } +} diff --git a/tests/common/java/android/net/metrics/ApfStatsTest.kt b/tests/common/java/android/net/metrics/ApfStatsTest.kt new file mode 100644 index 0000000000..46a8c8e5b5 --- /dev/null +++ b/tests/common/java/android/net/metrics/ApfStatsTest.kt @@ -0,0 +1,57 @@ +/* + * 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.metrics + +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.assertParcelSane +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class ApfStatsTest { + @Test + fun testBuilderAndParcel() { + val apfStats = ApfStats.Builder() + .setDurationMs(Long.MAX_VALUE) + .setReceivedRas(1) + .setMatchingRas(2) + .setDroppedRas(3) + .setZeroLifetimeRas(4) + .setParseErrors(5) + .setProgramUpdates(6) + .setProgramUpdatesAll(7) + .setProgramUpdatesAllowingMulticast(8) + .setMaxProgramSize(9) + .build() + + assertEquals(Long.MAX_VALUE, apfStats.durationMs) + assertEquals(1, apfStats.receivedRas) + assertEquals(2, apfStats.matchingRas) + assertEquals(3, apfStats.droppedRas) + assertEquals(4, apfStats.zeroLifetimeRas) + assertEquals(5, apfStats.parseErrors) + assertEquals(6, apfStats.programUpdates) + assertEquals(7, apfStats.programUpdatesAll) + assertEquals(8, apfStats.programUpdatesAllowingMulticast) + assertEquals(9, apfStats.maxProgramSize) + + assertParcelSane(apfStats, 10) + } +} diff --git a/tests/common/java/android/net/metrics/DhcpClientEventTest.kt b/tests/common/java/android/net/metrics/DhcpClientEventTest.kt new file mode 100644 index 0000000000..8d7a9c4050 --- /dev/null +++ b/tests/common/java/android/net/metrics/DhcpClientEventTest.kt @@ -0,0 +1,43 @@ +/* + * 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.metrics + +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.assertParcelSane +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +private const val FAKE_MESSAGE = "test" + +@RunWith(AndroidJUnit4::class) +@SmallTest +class DhcpClientEventTest { + @Test + fun testBuilderAndParcel() { + val dhcpClientEvent = DhcpClientEvent.Builder() + .setMsg(FAKE_MESSAGE) + .setDurationMs(Integer.MAX_VALUE) + .build() + + assertEquals(FAKE_MESSAGE, dhcpClientEvent.msg) + assertEquals(Integer.MAX_VALUE, dhcpClientEvent.durationMs) + + assertParcelSane(dhcpClientEvent, 2) + } +} diff --git a/tests/common/java/android/net/metrics/DhcpErrorEventTest.kt b/tests/common/java/android/net/metrics/DhcpErrorEventTest.kt new file mode 100644 index 0000000000..236f72eafb --- /dev/null +++ b/tests/common/java/android/net/metrics/DhcpErrorEventTest.kt @@ -0,0 +1,65 @@ +package android.net.metrics + +import android.net.metrics.DhcpErrorEvent.DHCP_INVALID_OPTION_LENGTH +import android.net.metrics.DhcpErrorEvent.errorCodeWithOption +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.parcelingRoundTrip +import java.lang.reflect.Modifier +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +private const val TEST_ERROR_CODE = 12345 +//DHCP Optional Type: DHCP Subnet Mask (Copy from DhcpPacket.java due to it's protected) +private const val DHCP_SUBNET_MASK = 1 + +@RunWith(AndroidJUnit4::class) +@SmallTest +class DhcpErrorEventTest { + + @Test + fun testConstructor() { + val event = DhcpErrorEvent(TEST_ERROR_CODE) + assertEquals(TEST_ERROR_CODE, event.errorCode) + } + + @Test + fun testParcelUnparcel() { + val event = DhcpErrorEvent(TEST_ERROR_CODE) + val parceled = parcelingRoundTrip(event) + assertEquals(TEST_ERROR_CODE, parceled.errorCode) + } + + @Test + fun testErrorCodeWithOption() { + val errorCode = errorCodeWithOption(DHCP_INVALID_OPTION_LENGTH, DHCP_SUBNET_MASK); + assertTrue((DHCP_INVALID_OPTION_LENGTH and errorCode) == DHCP_INVALID_OPTION_LENGTH); + assertTrue((DHCP_SUBNET_MASK and errorCode) == DHCP_SUBNET_MASK); + } + + @Test + fun testToString() { + val names = listOf("L2_ERROR", "L3_ERROR", "L4_ERROR", "DHCP_ERROR", "MISC_ERROR") + val errorFields = DhcpErrorEvent::class.java.declaredFields.filter { + it.type == Int::class.javaPrimitiveType + && Modifier.isPublic(it.modifiers) && Modifier.isStatic(it.modifiers) + && it.name !in names + } + + errorFields.forEach { + val intValue = it.getInt(null) + val stringValue = DhcpErrorEvent(intValue).toString() + assertTrue("Invalid string for error 0x%08X (field %s): %s".format(intValue, it.name, + stringValue), + stringValue.contains(it.name)) + } + } + + @Test + fun testToString_InvalidErrorCode() { + assertNotNull(DhcpErrorEvent(TEST_ERROR_CODE).toString()) + } +} diff --git a/tests/common/java/android/net/metrics/IpConnectivityLogTest.java b/tests/common/java/android/net/metrics/IpConnectivityLogTest.java new file mode 100644 index 0000000000..d4780d3a5d --- /dev/null +++ b/tests/common/java/android/net/metrics/IpConnectivityLogTest.java @@ -0,0 +1,161 @@ +/* + * 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.metrics; + +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import android.net.ConnectivityMetricsEvent; +import android.net.IIpConnectivityMetrics; +import android.net.Network; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.BitUtils; + +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 java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class IpConnectivityLogTest { + private static final int FAKE_NET_ID = 100; + private static final int[] FAKE_TRANSPORT_TYPES = BitUtils.unpackBits(TRANSPORT_WIFI); + private static final long FAKE_TIME_STAMP = System.currentTimeMillis(); + private static final String FAKE_INTERFACE_NAME = "test"; + private static final IpReachabilityEvent FAKE_EV = + new IpReachabilityEvent(IpReachabilityEvent.NUD_FAILED); + + @Mock IIpConnectivityMetrics mMockService; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testLoggingEvents() throws Exception { + IpConnectivityLog logger = new IpConnectivityLog(mMockService); + + assertTrue(logger.log(FAKE_EV)); + assertTrue(logger.log(FAKE_TIME_STAMP, FAKE_EV)); + assertTrue(logger.log(FAKE_NET_ID, FAKE_TRANSPORT_TYPES, FAKE_EV)); + assertTrue(logger.log(new Network(FAKE_NET_ID), FAKE_TRANSPORT_TYPES, FAKE_EV)); + assertTrue(logger.log(FAKE_INTERFACE_NAME, FAKE_EV)); + assertTrue(logger.log(makeExpectedEvent(FAKE_TIME_STAMP, FAKE_NET_ID, TRANSPORT_WIFI, + FAKE_INTERFACE_NAME))); + + List got = verifyEvents(6); + assertEventsEqual(makeExpectedEvent(got.get(0).timestamp, 0, 0, null), got.get(0)); + assertEventsEqual(makeExpectedEvent(FAKE_TIME_STAMP, 0, 0, null), got.get(1)); + assertEventsEqual(makeExpectedEvent(got.get(2).timestamp, FAKE_NET_ID, + TRANSPORT_WIFI, null), got.get(2)); + assertEventsEqual(makeExpectedEvent(got.get(3).timestamp, FAKE_NET_ID, + TRANSPORT_WIFI, null), got.get(3)); + assertEventsEqual(makeExpectedEvent(got.get(4).timestamp, 0, 0, FAKE_INTERFACE_NAME), + got.get(4)); + assertEventsEqual(makeExpectedEvent(FAKE_TIME_STAMP, FAKE_NET_ID, + TRANSPORT_WIFI, FAKE_INTERFACE_NAME), got.get(5)); + } + + @Test + public void testLoggingEventsWithMultipleCallers() throws Exception { + IpConnectivityLog logger = new IpConnectivityLog(mMockService); + + final int nCallers = 10; + final int nEvents = 10; + for (int n = 0; n < nCallers; n++) { + final int i = n; + new Thread() { + public void run() { + for (int j = 0; j < nEvents; j++) { + assertTrue(logger.log(makeExpectedEvent( + FAKE_TIME_STAMP + i * 100 + j, + FAKE_NET_ID + i * 100 + j, + ((i + j) % 2 == 0) ? TRANSPORT_WIFI : TRANSPORT_CELLULAR, + FAKE_INTERFACE_NAME))); + } + } + }.start(); + } + + List got = verifyEvents(nCallers * nEvents, 200); + Collections.sort(got, EVENT_COMPARATOR); + Iterator iter = got.iterator(); + for (int i = 0; i < nCallers; i++) { + for (int j = 0; j < nEvents; j++) { + final long expectedTimestamp = FAKE_TIME_STAMP + i * 100 + j; + final int expectedNetId = FAKE_NET_ID + i * 100 + j; + final long expectedTransports = + ((i + j) % 2 == 0) ? TRANSPORT_WIFI : TRANSPORT_CELLULAR; + assertEventsEqual(makeExpectedEvent(expectedTimestamp, expectedNetId, + expectedTransports, FAKE_INTERFACE_NAME), iter.next()); + } + } + } + + private List verifyEvents(int n, int timeoutMs) throws Exception { + ArgumentCaptor captor = + ArgumentCaptor.forClass(ConnectivityMetricsEvent.class); + verify(mMockService, timeout(timeoutMs).times(n)).logEvent(captor.capture()); + return captor.getAllValues(); + } + + private List verifyEvents(int n) throws Exception { + return verifyEvents(n, 10); + } + + + private ConnectivityMetricsEvent makeExpectedEvent(long timestamp, int netId, long transports, + String ifname) { + ConnectivityMetricsEvent ev = new ConnectivityMetricsEvent(); + ev.timestamp = timestamp; + ev.data = FAKE_EV; + ev.netId = netId; + ev.transports = transports; + ev.ifname = ifname; + return ev; + } + + /** Outer equality for ConnectivityMetricsEvent to avoid overriding equals() and hashCode(). */ + private void assertEventsEqual(ConnectivityMetricsEvent expected, + ConnectivityMetricsEvent got) { + assertEquals(expected.data, got.data); + assertEquals(expected.timestamp, got.timestamp); + assertEquals(expected.netId, got.netId); + assertEquals(expected.transports, got.transports); + assertEquals(expected.ifname, got.ifname); + } + + static final Comparator EVENT_COMPARATOR = + Comparator.comparingLong((ev) -> ev.timestamp); +} diff --git a/tests/common/java/android/net/metrics/IpManagerEventTest.kt b/tests/common/java/android/net/metrics/IpManagerEventTest.kt new file mode 100644 index 0000000000..64be50837f --- /dev/null +++ b/tests/common/java/android/net/metrics/IpManagerEventTest.kt @@ -0,0 +1,39 @@ +/* + * 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.metrics + +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.assertParcelSane +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class IpManagerEventTest { + @Test + fun testConstructorAndParcel() { + (IpManagerEvent.PROVISIONING_OK..IpManagerEvent.ERROR_INTERFACE_NOT_FOUND).forEach { + val ipManagerEvent = IpManagerEvent(it, Long.MAX_VALUE) + assertEquals(it, ipManagerEvent.eventType) + assertEquals(Long.MAX_VALUE, ipManagerEvent.durationMs) + + assertParcelSane(ipManagerEvent, 2) + } + } +} diff --git a/tests/common/java/android/net/metrics/IpReachabilityEventTest.kt b/tests/common/java/android/net/metrics/IpReachabilityEventTest.kt new file mode 100644 index 0000000000..55b5e492dd --- /dev/null +++ b/tests/common/java/android/net/metrics/IpReachabilityEventTest.kt @@ -0,0 +1,38 @@ +/* + * 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.metrics + +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.assertParcelSane +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class IpReachabilityEventTest { + @Test + fun testConstructorAndParcel() { + (IpReachabilityEvent.PROBE..IpReachabilityEvent.PROVISIONING_LOST_ORGANIC).forEach { + val ipReachabilityEvent = IpReachabilityEvent(it) + assertEquals(it, ipReachabilityEvent.eventType) + + assertParcelSane(ipReachabilityEvent, 1) + } + } +} diff --git a/tests/common/java/android/net/metrics/NetworkEventTest.kt b/tests/common/java/android/net/metrics/NetworkEventTest.kt new file mode 100644 index 0000000000..41430b03a1 --- /dev/null +++ b/tests/common/java/android/net/metrics/NetworkEventTest.kt @@ -0,0 +1,43 @@ +/* + * 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.metrics + +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.assertParcelSane +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class NetworkEventTest { + @Test + fun testConstructorAndParcel() { + (NetworkEvent.NETWORK_CONNECTED..NetworkEvent.NETWORK_PARTIAL_CONNECTIVITY).forEach { + var networkEvent = NetworkEvent(it) + assertEquals(it, networkEvent.eventType) + assertEquals(0, networkEvent.durationMs) + + networkEvent = NetworkEvent(it, Long.MAX_VALUE) + assertEquals(it, networkEvent.eventType) + assertEquals(Long.MAX_VALUE, networkEvent.durationMs) + + assertParcelSane(networkEvent, 2) + } + } +} diff --git a/tests/common/java/android/net/metrics/RaEventTest.kt b/tests/common/java/android/net/metrics/RaEventTest.kt new file mode 100644 index 0000000000..d9b720332f --- /dev/null +++ b/tests/common/java/android/net/metrics/RaEventTest.kt @@ -0,0 +1,72 @@ +/* + * 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.metrics + +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.assertParcelSane +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +private const val NO_LIFETIME: Long = -1L + +@RunWith(AndroidJUnit4::class) +@SmallTest +class RaEventTest { + @Test + fun testConstructorAndParcel() { + var raEvent = RaEvent.Builder().build() + assertEquals(NO_LIFETIME, raEvent.routerLifetime) + assertEquals(NO_LIFETIME, raEvent.prefixValidLifetime) + assertEquals(NO_LIFETIME, raEvent.prefixPreferredLifetime) + assertEquals(NO_LIFETIME, raEvent.routeInfoLifetime) + assertEquals(NO_LIFETIME, raEvent.rdnssLifetime) + assertEquals(NO_LIFETIME, raEvent.dnsslLifetime) + + raEvent = RaEvent.Builder() + .updateRouterLifetime(1) + .updatePrefixValidLifetime(2) + .updatePrefixPreferredLifetime(3) + .updateRouteInfoLifetime(4) + .updateRdnssLifetime(5) + .updateDnsslLifetime(6) + .build() + assertEquals(1, raEvent.routerLifetime) + assertEquals(2, raEvent.prefixValidLifetime) + assertEquals(3, raEvent.prefixPreferredLifetime) + assertEquals(4, raEvent.routeInfoLifetime) + assertEquals(5, raEvent.rdnssLifetime) + assertEquals(6, raEvent.dnsslLifetime) + + raEvent = RaEvent.Builder() + .updateRouterLifetime(Long.MIN_VALUE) + .updateRouterLifetime(Long.MAX_VALUE) + .build() + assertEquals(Long.MIN_VALUE, raEvent.routerLifetime) + + raEvent = RaEvent(1, 2, 3, 4, 5, 6) + assertEquals(1, raEvent.routerLifetime) + assertEquals(2, raEvent.prefixValidLifetime) + assertEquals(3, raEvent.prefixPreferredLifetime) + assertEquals(4, raEvent.routeInfoLifetime) + assertEquals(5, raEvent.rdnssLifetime) + assertEquals(6, raEvent.dnsslLifetime) + + assertParcelSane(raEvent, 6) + } +} diff --git a/tests/common/java/android/net/metrics/ValidationProbeEventTest.kt b/tests/common/java/android/net/metrics/ValidationProbeEventTest.kt new file mode 100644 index 0000000000..51c0d41bf4 --- /dev/null +++ b/tests/common/java/android/net/metrics/ValidationProbeEventTest.kt @@ -0,0 +1,72 @@ +/* + * 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.metrics + +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.assertParcelSane +import java.lang.reflect.Modifier +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +private const val FIRST_VALIDATION: Int = 1 shl 8 +private const val REVALIDATION: Int = 2 shl 8 + +@RunWith(AndroidJUnit4::class) +@SmallTest +class ValidationProbeEventTest { + private infix fun Int.hasType(type: Int) = (type and this) == type + + @Test + fun testBuilderAndParcel() { + var validationProbeEvent = ValidationProbeEvent.Builder() + .setProbeType(ValidationProbeEvent.PROBE_DNS, false).build() + + assertTrue(validationProbeEvent.probeType hasType REVALIDATION) + + validationProbeEvent = ValidationProbeEvent.Builder() + .setDurationMs(Long.MAX_VALUE) + .setProbeType(ValidationProbeEvent.PROBE_DNS, true) + .setReturnCode(ValidationProbeEvent.DNS_SUCCESS) + .build() + + assertEquals(Long.MAX_VALUE, validationProbeEvent.durationMs) + assertTrue(validationProbeEvent.probeType hasType ValidationProbeEvent.PROBE_DNS) + assertTrue(validationProbeEvent.probeType hasType FIRST_VALIDATION) + assertEquals(ValidationProbeEvent.DNS_SUCCESS, validationProbeEvent.returnCode) + + assertParcelSane(validationProbeEvent, 3) + } + + @Test + fun testGetProbeName() { + val probeFields = ValidationProbeEvent::class.java.declaredFields.filter { + it.type == Int::class.javaPrimitiveType + && Modifier.isPublic(it.modifiers) && Modifier.isStatic(it.modifiers) + && it.name.contains("PROBE") + } + + probeFields.forEach { + val intValue = it.getInt(null) + val stringValue = ValidationProbeEvent.getProbeName(intValue) + assertEquals(it.name, stringValue) + } + + } +} diff --git a/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt new file mode 100644 index 0000000000..7b22e45db9 --- /dev/null +++ b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2020 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.netstats + +import android.net.NetworkStats +import android.net.NetworkStats.DEFAULT_NETWORK_NO +import android.net.NetworkStats.DEFAULT_NETWORK_YES +import android.net.NetworkStats.Entry +import android.net.NetworkStats.IFACE_VT +import android.net.NetworkStats.METERED_NO +import android.net.NetworkStats.METERED_YES +import android.net.NetworkStats.ROAMING_NO +import android.net.NetworkStats.ROAMING_YES +import android.net.NetworkStats.SET_DEFAULT +import android.net.NetworkStats.SET_FOREGROUND +import android.net.NetworkStats.TAG_NONE +import android.os.Build +import androidx.test.filters.SmallTest +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.assertFieldCountEquals +import com.android.testutils.assertNetworkStatsEquals +import com.android.testutils.assertParcelingIsLossless +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import kotlin.test.assertEquals + +@RunWith(JUnit4::class) +@SmallTest +class NetworkStatsApiTest { + @Rule + @JvmField + val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q) + + private val testStatsEmpty = NetworkStats(0L, 0) + + // Note that these variables need to be initialized outside of constructor, initialize + // here with methods that don't exist in Q devices will result in crash. + + // stats1 and stats2 will have some entries with common keys, which are expected to + // be merged if performing add on these 2 stats. + private lateinit var testStats1: NetworkStats + private lateinit var testStats2: NetworkStats + + // This is a result of adding stats1 and stats2, while the merging of common key items is + // subject to test later, this should not be initialized with for a loop to add stats1 + // and stats2 above. + private lateinit var testStats3: NetworkStats + + companion object { + private const val TEST_IFACE = "test0" + private const val TEST_UID1 = 1001 + private const val TEST_UID2 = 1002 + } + + @Before + fun setUp() { + testStats1 = NetworkStats(0L, 0) + // Entries which only appear in set1. + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, 20, 3, 57, 40, 3)) + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_YES, DEFAULT_NETWORK_NO, 31, 7, 24, 5, 8)) + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_YES, ROAMING_NO, DEFAULT_NETWORK_NO, 25, 3, 47, 8, 2)) + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 37, 52, 1, 10, 4)) + // Entries which are common for set1 and set2. + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 101, 2, 103, 4, 5)) + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 17, 2, 11, 1, 0)) + .addEntry(Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 40, 1, 0, 0, 8)) + .addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 1, 6, 2, 0)) + assertEquals(8, testStats1.size()) + + testStats2 = NetworkStats(0L, 0) + // Entries which are common for set1 and set2. + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 15, 2, 31, 1)) + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13, 61, 10, 1, 45)) + .addEntry(Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 11, 2, 3, 4, 7)) + .addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 4, 3, 2, 1, 0)) + // Entry which only appears in set2. + .addEntry(Entry(IFACE_VT, TEST_UID2, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 2, 3, 7, 8, 0)) + assertEquals(5, testStats2.size()) + + testStats3 = NetworkStats(0L, 9) + // Entries which are unique either in stats1 or stats2. + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 101, 2, 103, 4, 5)) + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_YES, DEFAULT_NETWORK_NO, 31, 7, 24, 5, 8)) + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_YES, ROAMING_NO, DEFAULT_NETWORK_NO, 25, 3, 47, 8, 2)) + .addEntry(Entry(IFACE_VT, TEST_UID2, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 2, 3, 7, 8, 0)) + // Entries which are common for stats1 and stats2 are being merged. + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, 20, 3, 57, 40, 3)) + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 20, 17, 13, 32, 1)) + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 50, 113, 11, 11, 49)) + .addEntry(Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 51, 3, 3, 4, 15)) + .addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 7, 4, 8, 3, 0)) + assertEquals(9, testStats3.size()) + } + + @Test + fun testAddEntry() { + val expectedEntriesInStats2 = arrayOf( + Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 15, 2, 31, 1), + Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13, 61, 10, 1, 45), + Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 11, 2, 3, 4, 7), + Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 4, 3, 2, 1, 0), + Entry(IFACE_VT, TEST_UID2, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 2, 3, 7, 8, 0)) + + // While testStats* are already initialized with addEntry, verify content added + // matches expectation. + for (i in expectedEntriesInStats2.indices) { + val entry = testStats2.getValues(i, null) + assertEquals(expectedEntriesInStats2[i], entry) + } + + // Verify entry updated with addEntry. + val stats = testStats2.addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12, -5, 7, 0, 9)) + assertEquals(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 16, -2, 9, 1, 9), + stats.getValues(3, null)) + } + + @Test + fun testAdd() { + var stats = NetworkStats(0L, 0) + assertNetworkStatsEquals(testStatsEmpty, stats) + stats = stats.add(testStats2) + assertNetworkStatsEquals(testStats2, stats) + stats = stats.add(testStats1) + // EMPTY + STATS2 + STATS1 = STATS3 + assertNetworkStatsEquals(testStats3, stats) + } + + @Test + fun testParcelUnparcel() { + assertParcelingIsLossless(testStatsEmpty) + assertParcelingIsLossless(testStats1) + assertParcelingIsLossless(testStats2) + assertFieldCountEquals(15, NetworkStats::class.java) + } + + @Test + fun testDescribeContents() { + assertEquals(0, testStatsEmpty.describeContents()) + assertEquals(0, testStats1.describeContents()) + assertEquals(0, testStats2.describeContents()) + assertEquals(0, testStats3.describeContents()) + } + + @Test + fun testSubtract() { + // STATS3 - STATS2 = STATS1 + assertNetworkStatsEquals(testStats1, testStats3.subtract(testStats2)) + // STATS3 - STATS1 = STATS2 + assertNetworkStatsEquals(testStats2, testStats3.subtract(testStats1)) + } + + @Test + fun testMethodsDontModifyReceiver() { + listOf(testStatsEmpty, testStats1, testStats2, testStats3).forEach { + val origStats = it.clone() + it.addEntry(Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13, 61, 10, 1, 45)) + it.add(testStats3) + it.subtract(testStats1) + assertNetworkStatsEquals(origStats, it) + } + } +} \ No newline at end of file diff --git a/tests/common/java/android/net/util/SocketUtilsTest.kt b/tests/common/java/android/net/util/SocketUtilsTest.kt new file mode 100644 index 0000000000..aaf97f3688 --- /dev/null +++ b/tests/common/java/android/net/util/SocketUtilsTest.kt @@ -0,0 +1,90 @@ +/* + * 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.util + +import android.os.Build +import android.system.NetlinkSocketAddress +import android.system.Os +import android.system.OsConstants.AF_INET +import android.system.OsConstants.ETH_P_ALL +import android.system.OsConstants.IPPROTO_UDP +import android.system.OsConstants.RTMGRP_NEIGH +import android.system.OsConstants.SOCK_DGRAM +import android.system.PacketSocketAddress +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +private const val TEST_INDEX = 123 +private const val TEST_PORT = 555 +private const val FF_BYTE = 0xff.toByte() + +@RunWith(AndroidJUnit4::class) +@SmallTest +class SocketUtilsTest { + @Rule @JvmField + val ignoreRule = DevSdkIgnoreRule() + + @Test + fun testMakeNetlinkSocketAddress() { + val nlAddress = SocketUtils.makeNetlinkSocketAddress(TEST_PORT, RTMGRP_NEIGH) + if (nlAddress is NetlinkSocketAddress) { + assertEquals(TEST_PORT, nlAddress.getPortId()) + assertEquals(RTMGRP_NEIGH, nlAddress.getGroupsMask()) + } else { + fail("Not NetlinkSocketAddress object") + } + } + + @Test + fun testMakePacketSocketAddress_Q() { + val pkAddress = SocketUtils.makePacketSocketAddress(ETH_P_ALL, TEST_INDEX) + assertTrue("Not PacketSocketAddress object", pkAddress is PacketSocketAddress) + + val pkAddress2 = SocketUtils.makePacketSocketAddress(TEST_INDEX, ByteArray(6) { FF_BYTE }) + assertTrue("Not PacketSocketAddress object", pkAddress2 is PacketSocketAddress) + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testMakePacketSocketAddress() { + val pkAddress = SocketUtils.makePacketSocketAddress( + ETH_P_ALL, TEST_INDEX, ByteArray(6) { FF_BYTE }) + assertTrue("Not PacketSocketAddress object", pkAddress is PacketSocketAddress) + } + + @Test + fun testCloseSocket() { + // Expect no exception happening with null object. + SocketUtils.closeSocket(null) + + val fd = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) + assertTrue(fd.valid()) + SocketUtils.closeSocket(fd) + assertFalse(fd.valid()) + // Expecting socket should be still invalid even closed socket again. + SocketUtils.closeSocket(fd) + assertFalse(fd.valid()) + } +} diff --git a/tests/deflake/Android.bp b/tests/deflake/Android.bp new file mode 100644 index 0000000000..58ece37ef6 --- /dev/null +++ b/tests/deflake/Android.bp @@ -0,0 +1,39 @@ +// +// 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +java_test_host { + name: "FrameworksNetDeflakeTest", + srcs: ["src/**/*.kt"], + libs: [ + "junit", + "tradefed", + ], + static_libs: [ + "kotlin-test", + "net-host-tests-utils", + ], + data: [":FrameworksNetTests"], + test_suites: ["device-tests"], +} diff --git a/tests/deflake/src/com/android/server/net/FrameworksNetDeflakeTest.kt b/tests/deflake/src/com/android/server/net/FrameworksNetDeflakeTest.kt new file mode 100644 index 0000000000..62855255fe --- /dev/null +++ b/tests/deflake/src/com/android/server/net/FrameworksNetDeflakeTest.kt @@ -0,0 +1,28 @@ +/* + * 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 com.android.server.net + +import com.android.testutils.host.DeflakeHostTestBase +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner +import org.junit.runner.RunWith + +@RunWith(DeviceJUnit4ClassRunner::class) +class FrameworksNetDeflakeTest: DeflakeHostTestBase() { + override val runCount = 20 + override val testApkFilename = "FrameworksNetTests.apk" + override val testClasses = listOf("com.android.server.ConnectivityServiceTest") +} \ No newline at end of file diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp new file mode 100644 index 0000000000..56f9df78c8 --- /dev/null +++ b/tests/integration/Android.bp @@ -0,0 +1,76 @@ +// +// 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "FrameworksNetIntegrationTests", + platform_apis: true, + certificate: "platform", + srcs: [ + "src/**/*.kt", + "src/**/*.aidl", + ], + libs: [ + "android.test.mock", + ], + static_libs: [ + "NetworkStackApiStableLib", + "androidx.test.ext.junit", + "frameworks-net-integration-testutils", + "kotlin-reflect", + "mockito-target-extended-minus-junit4", + "net-tests-utils", + "service-connectivity", + "services.core", + "services.net", + "testables", + ], + test_suites: ["device-tests"], + use_embedded_native_libs: true, + jni_libs: [ + // For mockito extended + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + // android_library does not include JNI libs: include NetworkStack dependencies here + "libnativehelper_compat_libc++", + "libnetworkstackutilsjni", + ], +} + +// Utilities for testing framework code both in integration and unit tests. +java_library { + name: "frameworks-net-integration-testutils", + srcs: ["util/**/*.java", "util/**/*.kt"], + static_libs: [ + "androidx.annotation_annotation", + "androidx.test.rules", + "junit", + "net-tests-utils", + ], + libs: [ + "service-connectivity", + "services.core", + "services.net", + ], +} diff --git a/tests/integration/AndroidManifest.xml b/tests/integration/AndroidManifest.xml new file mode 100644 index 0000000000..2e13689357 --- /dev/null +++ b/tests/integration/AndroidManifest.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/integration/res/values/config.xml b/tests/integration/res/values/config.xml new file mode 100644 index 0000000000..2c8046ffd7 --- /dev/null +++ b/tests/integration/res/values/config.xml @@ -0,0 +1,15 @@ + + + + 12500 + http://test.android.com + https://secure.test.android.com + + http://fallback1.android.com + http://fallback2.android.com + + + + diff --git a/tests/integration/src/android/net/TestNetworkStackClient.kt b/tests/integration/src/android/net/TestNetworkStackClient.kt new file mode 100644 index 0000000000..01eb514a1c --- /dev/null +++ b/tests/integration/src/android/net/TestNetworkStackClient.kt @@ -0,0 +1,80 @@ +/* + * 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 + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.os.IBinder +import com.android.server.net.integrationtests.TestNetworkStackService +import org.mockito.Mockito.any +import org.mockito.Mockito.spy +import org.mockito.Mockito.timeout +import org.mockito.Mockito.verify +import kotlin.test.fail + +const val TEST_ACTION_SUFFIX = ".Test" + +class TestNetworkStackClient(context: Context) : NetworkStackClient(TestDependencies(context)) { + // TODO: consider switching to TrackRecord for more expressive checks + private val lastCallbacks = HashMap() + + private class TestDependencies(private val context: Context) : Dependencies { + override fun addToServiceManager(service: IBinder) = Unit + override fun checkCallerUid() = Unit + + override fun getConnectivityModuleConnector(): ConnectivityModuleConnector { + return ConnectivityModuleConnector { _, _, _, inSystemProcess -> + getNetworkStackIntent(inSystemProcess) + }.also { it.init(context) } + } + + private fun getNetworkStackIntent(inSystemProcess: Boolean): Intent? { + // Simulate out-of-system-process config: in-process service not found (null intent) + if (inSystemProcess) return null + val intent = Intent(INetworkStackConnector::class.qualifiedName + TEST_ACTION_SUFFIX) + val serviceName = TestNetworkStackService::class.qualifiedName + ?: fail("TestNetworkStackService name not found") + intent.component = ComponentName(context.packageName, serviceName) + return intent + } + } + + // base may be an instance of an inaccessible subclass, so non-spyable. + // Use a known open class that delegates to the original instance for all methods except + // asBinder. asBinder needs to use its own non-delegated implementation as otherwise it would + // return a binder token to a class that is not spied on. + open class NetworkMonitorCallbacksWrapper(private val base: INetworkMonitorCallbacks) : + INetworkMonitorCallbacks.Stub(), INetworkMonitorCallbacks by base { + // asBinder is implemented by both base class and delegate: specify explicitly + override fun asBinder(): IBinder { + return super.asBinder() + } + } + + override fun makeNetworkMonitor(network: Network, name: String?, cb: INetworkMonitorCallbacks) { + val cbSpy = spy(NetworkMonitorCallbacksWrapper(cb)) + lastCallbacks[network] = cbSpy + super.makeNetworkMonitor(network, name, cbSpy) + } + + fun verifyNetworkMonitorCreated(network: Network, timeoutMs: Long) { + val cb = lastCallbacks[network] + ?: fail("NetworkMonitor for network $network not requested") + verify(cb, timeout(timeoutMs)).onNetworkMonitorCreated(any()) + } +} \ No newline at end of file diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt new file mode 100644 index 0000000000..b6e42743e2 --- /dev/null +++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt @@ -0,0 +1,258 @@ +/* + * 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 com.android.server.net.integrationtests + +import android.app.usage.NetworkStatsManager +import android.content.ComponentName +import android.content.Context +import android.content.Context.BIND_AUTO_CREATE +import android.content.Context.BIND_IMPORTANT +import android.content.Intent +import android.content.ServiceConnection +import android.net.ConnectivityManager +import android.net.IDnsResolver +import android.net.INetd +import android.net.LinkProperties +import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL +import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET +import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED +import android.net.NetworkCapabilities.TRANSPORT_CELLULAR +import android.net.NetworkRequest +import android.net.TestNetworkStackClient +import android.net.Uri +import android.net.metrics.IpConnectivityLog +import android.os.ConditionVariable +import android.os.IBinder +import android.os.SystemConfigManager +import android.os.UserHandle +import android.testing.TestableContext +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.ConnectivityService +import com.android.server.NetworkAgentWrapper +import com.android.server.TestNetIdManager +import com.android.server.connectivity.MockableSystemProperties +import com.android.server.connectivity.ProxyTracker +import com.android.testutils.TestableNetworkCallback +import org.junit.After +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.AdditionalAnswers +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.doNothing +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.eq +import org.mockito.Mockito.mock +import org.mockito.Mockito.spy +import org.mockito.MockitoAnnotations +import org.mockito.Spy +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.test.fail + +const val SERVICE_BIND_TIMEOUT_MS = 5_000L +const val TEST_TIMEOUT_MS = 10_000L + +/** + * Test that exercises an instrumented version of ConnectivityService against an instrumented + * NetworkStack in a different test process. + */ +@RunWith(AndroidJUnit4::class) +class ConnectivityServiceIntegrationTest { + // lateinit used here for mocks as they need to be reinitialized between each test and the test + // should crash if they are used before being initialized. + @Mock + private lateinit var statsManager: NetworkStatsManager + @Mock + private lateinit var log: IpConnectivityLog + @Mock + private lateinit var netd: INetd + @Mock + private lateinit var dnsResolver: IDnsResolver + @Mock + private lateinit var systemConfigManager: SystemConfigManager + @Spy + private var context = TestableContext(realContext) + + // lateinit for these three classes under test, as they should be reset to a different instance + // for every test but should always be initialized before use (or the test should crash). + private lateinit var networkStackClient: TestNetworkStackClient + private lateinit var service: ConnectivityService + private lateinit var cm: ConnectivityManager + + companion object { + // lateinit for this binder token, as it must be initialized before any test code is run + // and use of it before init should crash the test. + private lateinit var nsInstrumentation: INetworkStackInstrumentation + private val bindingCondition = ConditionVariable(false) + + private val realContext get() = InstrumentationRegistry.getInstrumentation().context + private val httpProbeUrl get() = + realContext.getResources().getString(R.string.config_captive_portal_http_url) + private val httpsProbeUrl get() = + realContext.getResources().getString(R.string.config_captive_portal_https_url) + + private class InstrumentationServiceConnection : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + Log.i("TestNetworkStack", "Service connected") + try { + if (service == null) fail("Error binding to NetworkStack instrumentation") + if (::nsInstrumentation.isInitialized) fail("Service already connected") + nsInstrumentation = INetworkStackInstrumentation.Stub.asInterface(service) + } finally { + bindingCondition.open() + } + } + + override fun onServiceDisconnected(name: ComponentName?) = Unit + } + + @BeforeClass + @JvmStatic + fun setUpClass() { + val intent = Intent(realContext, NetworkStackInstrumentationService::class.java) + intent.action = INetworkStackInstrumentation::class.qualifiedName + assertTrue(realContext.bindService(intent, InstrumentationServiceConnection(), + BIND_AUTO_CREATE or BIND_IMPORTANT), + "Error binding to instrumentation service") + assertTrue(bindingCondition.block(SERVICE_BIND_TIMEOUT_MS), + "Timed out binding to instrumentation service " + + "after $SERVICE_BIND_TIMEOUT_MS ms") + } + } + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + val asUserCtx = mock(Context::class.java, AdditionalAnswers.delegatesTo(context)) + doReturn(UserHandle.ALL).`when`(asUserCtx).user + doReturn(asUserCtx).`when`(context).createContextAsUser(eq(UserHandle.ALL), anyInt()) + doNothing().`when`(context).sendStickyBroadcast(any(), any()) + doReturn(Context.SYSTEM_CONFIG_SERVICE).`when`(context) + .getSystemServiceName(SystemConfigManager::class.java) + doReturn(systemConfigManager).`when`(context) + .getSystemService(Context.SYSTEM_CONFIG_SERVICE) + doReturn(IntArray(0)).`when`(systemConfigManager).getSystemPermissionUids(anyString()) + + networkStackClient = TestNetworkStackClient(realContext) + networkStackClient.init() + networkStackClient.start() + + service = TestConnectivityService(makeDependencies()) + cm = ConnectivityManager(context, service) + context.addMockSystemService(Context.CONNECTIVITY_SERVICE, cm) + context.addMockSystemService(Context.NETWORK_STATS_SERVICE, statsManager) + + service.systemReadyInternal() + } + + private inner class TestConnectivityService(deps: Dependencies) : ConnectivityService( + context, dnsResolver, log, netd, deps) + + private fun makeDependencies(): ConnectivityService.Dependencies { + val deps = spy(ConnectivityService.Dependencies()) + doReturn(networkStackClient).`when`(deps).networkStack + doReturn(mock(ProxyTracker::class.java)).`when`(deps).makeProxyTracker(any(), any()) + doReturn(mock(MockableSystemProperties::class.java)).`when`(deps).systemProperties + doReturn(TestNetIdManager()).`when`(deps).makeNetIdManager() + return deps + } + + @After + fun tearDown() { + nsInstrumentation.clearAllState() + } + + @Test + fun testValidation() { + val request = NetworkRequest.Builder() + .clearCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .build() + val testCallback = TestableNetworkCallback() + + cm.registerNetworkCallback(request, testCallback) + nsInstrumentation.addHttpResponse(HttpResponse(httpProbeUrl, responseCode = 204)) + nsInstrumentation.addHttpResponse(HttpResponse(httpsProbeUrl, responseCode = 204)) + + val na = NetworkAgentWrapper(TRANSPORT_CELLULAR, LinkProperties(), null /* ncTemplate */, + context) + networkStackClient.verifyNetworkMonitorCreated(na.network, TEST_TIMEOUT_MS) + + na.addCapability(NET_CAPABILITY_INTERNET) + na.connect() + + testCallback.expectAvailableThenValidatedCallbacks(na.network, TEST_TIMEOUT_MS) + assertEquals(2, nsInstrumentation.getRequestUrls().size) + } + + @Test + fun testCapportApi() { + val request = NetworkRequest.Builder() + .clearCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .build() + val testCb = TestableNetworkCallback() + val apiUrl = "https://capport.android.com" + + cm.registerNetworkCallback(request, testCb) + nsInstrumentation.addHttpResponse(HttpResponse( + apiUrl, + """ + |{ + | "captive": true, + | "user-portal-url": "https://login.capport.android.com", + | "venue-info-url": "https://venueinfo.capport.android.com" + |} + """.trimMargin())) + + // Tests will fail if a non-mocked query is received: mock the HTTPS probe, but not the + // HTTP probe as it should not be sent. + // Even if the HTTPS probe succeeds, a portal should be detected as the API takes precedence + // in that case. + nsInstrumentation.addHttpResponse(HttpResponse(httpsProbeUrl, responseCode = 204)) + + val lp = LinkProperties() + lp.captivePortalApiUrl = Uri.parse(apiUrl) + val na = NetworkAgentWrapper(TRANSPORT_CELLULAR, lp, null /* ncTemplate */, context) + networkStackClient.verifyNetworkMonitorCreated(na.network, TEST_TIMEOUT_MS) + + na.addCapability(NET_CAPABILITY_INTERNET) + na.connect() + + testCb.expectAvailableCallbacks(na.network, validated = false, tmt = TEST_TIMEOUT_MS) + + val capportData = testCb.expectLinkPropertiesThat(na, TEST_TIMEOUT_MS) { + it.captivePortalData != null + }.lp.captivePortalData + assertNotNull(capportData) + assertTrue(capportData.isCaptive) + assertEquals(Uri.parse("https://login.capport.android.com"), capportData.userPortalUrl) + assertEquals(Uri.parse("https://venueinfo.capport.android.com"), capportData.venueInfoUrl) + + val nc = testCb.expectCapabilitiesWith(NET_CAPABILITY_CAPTIVE_PORTAL, na, TEST_TIMEOUT_MS) + assertFalse(nc.hasCapability(NET_CAPABILITY_VALIDATED)) + } +} \ No newline at end of file diff --git a/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.aidl b/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.aidl new file mode 100644 index 0000000000..9a2bcfea76 --- /dev/null +++ b/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.aidl @@ -0,0 +1,19 @@ +/* + * 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 com.android.server.net.integrationtests; + +parcelable HttpResponse; \ No newline at end of file diff --git a/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.kt b/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.kt new file mode 100644 index 0000000000..e2063138fe --- /dev/null +++ b/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.kt @@ -0,0 +1,49 @@ +/* + * 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 com.android.server.net.integrationtests + +import android.os.Parcel +import android.os.Parcelable + +data class HttpResponse( + val requestUrl: String, + val responseCode: Int, + val content: String = "", + val redirectUrl: String? = null +) : Parcelable { + constructor(p: Parcel): this(p.readString(), p.readInt(), p.readString(), p.readString()) + constructor(requestUrl: String, contentBody: String): this( + requestUrl, + responseCode = 200, + content = contentBody, + redirectUrl = null) + + override fun writeToParcel(dest: Parcel, flags: Int) { + with(dest) { + writeString(requestUrl) + writeInt(responseCode) + writeString(content) + writeString(redirectUrl) + } + } + + override fun describeContents() = 0 + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(source: Parcel) = HttpResponse(source) + override fun newArray(size: Int) = arrayOfNulls(size) + } +} \ No newline at end of file diff --git a/tests/integration/src/com/android/server/net/integrationtests/INetworkStackInstrumentation.aidl b/tests/integration/src/com/android/server/net/integrationtests/INetworkStackInstrumentation.aidl new file mode 100644 index 0000000000..efc58add9c --- /dev/null +++ b/tests/integration/src/com/android/server/net/integrationtests/INetworkStackInstrumentation.aidl @@ -0,0 +1,25 @@ +/* + * 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 com.android.server.net.integrationtests; + +import com.android.server.net.integrationtests.HttpResponse; + +interface INetworkStackInstrumentation { + void clearAllState(); + void addHttpResponse(in HttpResponse response); + List getRequestUrls(); +} \ No newline at end of file diff --git a/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt b/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt new file mode 100644 index 0000000000..e807952cec --- /dev/null +++ b/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt @@ -0,0 +1,84 @@ +/* + * 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 com.android.server.net.integrationtests + +import android.app.Service +import android.content.Intent +import java.net.URL +import java.util.Collections +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentLinkedQueue +import kotlin.collections.ArrayList +import kotlin.test.fail + +/** + * An instrumentation interface for the NetworkStack that allows controlling behavior to + * facilitate integration tests. + */ +class NetworkStackInstrumentationService : Service() { + override fun onBind(intent: Intent) = InstrumentationConnector.asBinder() + + object InstrumentationConnector : INetworkStackInstrumentation.Stub() { + private val httpResponses = ConcurrentHashMap>() + .run { + withDefault { key -> getOrPut(key) { ConcurrentLinkedQueue() } } + } + private val httpRequestUrls = Collections.synchronizedList(ArrayList()) + + /** + * Called when an HTTP request is being processed by NetworkMonitor. Returns the response + * that should be simulated. + */ + fun processRequest(url: URL): HttpResponse { + val strUrl = url.toString() + httpRequestUrls.add(strUrl) + return httpResponses[strUrl]?.poll() + ?: fail("No mocked response for request: $strUrl. " + + "Mocked URL keys are: ${httpResponses.keys}") + } + + /** + * Clear all state of this connector. This is intended for use between two tests, so all + * state should be reset as if the connector was just created. + */ + override fun clearAllState() { + httpResponses.clear() + httpRequestUrls.clear() + } + + /** + * Add a response to a future HTTP request. + * + *

For any subsequent HTTP/HTTPS query, the first response with a matching URL will be + * used to mock the query response. + * + *

All requests that are expected to be sent must have a mock response: if an unexpected + * request is seen, the test will fail. + */ + override fun addHttpResponse(response: HttpResponse) { + httpResponses.getValue(response.requestUrl).add(response) + } + + /** + * Get the ordered list of request URLs that have been sent by NetworkMonitor, and were + * answered based on mock responses. + */ + override fun getRequestUrls(): List { + return ArrayList(httpRequestUrls) + } + } +} \ No newline at end of file diff --git a/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt b/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt new file mode 100644 index 0000000000..eff66584d6 --- /dev/null +++ b/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt @@ -0,0 +1,100 @@ +/* + * 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 com.android.server.net.integrationtests + +import android.app.Service +import android.content.Context +import android.content.Intent +import android.net.INetworkMonitorCallbacks +import android.net.Network +import android.net.metrics.IpConnectivityLog +import android.net.util.SharedLog +import android.os.IBinder +import com.android.networkstack.netlink.TcpSocketTracker +import com.android.server.NetworkStackService +import com.android.server.NetworkStackService.NetworkMonitorConnector +import com.android.server.NetworkStackService.NetworkStackConnector +import com.android.server.connectivity.NetworkMonitor +import com.android.server.net.integrationtests.NetworkStackInstrumentationService.InstrumentationConnector +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.spy +import java.io.ByteArrayInputStream +import java.net.HttpURLConnection +import java.net.URL +import java.net.URLConnection +import java.nio.charset.StandardCharsets + +private const val TEST_NETID = 42 + +/** + * Android service that can return an [android.net.INetworkStackConnector] which can be instrumented + * through [NetworkStackInstrumentationService]. + * Useful in tests to create test instrumented NetworkStack components that can receive + * instrumentation commands through [NetworkStackInstrumentationService]. + */ +class TestNetworkStackService : Service() { + override fun onBind(intent: Intent): IBinder = TestNetworkStackConnector(makeTestContext()) + + private fun makeTestContext() = spy(applicationContext).also { + doReturn(mock(IBinder::class.java)).`when`(it).getSystemService(Context.NETD_SERVICE) + } + + private class TestPermissionChecker : NetworkStackService.PermissionChecker() { + override fun enforceNetworkStackCallingPermission() = Unit + } + + private class NetworkMonitorDeps(private val privateDnsBypassNetwork: Network) : + NetworkMonitor.Dependencies() { + override fun getPrivateDnsBypassNetwork(network: Network?) = privateDnsBypassNetwork + } + + private inner class TestNetworkStackConnector(context: Context) : NetworkStackConnector( + context, TestPermissionChecker(), NetworkStackService.Dependencies()) { + + private val network = Network(TEST_NETID) + private val privateDnsBypassNetwork = TestNetwork(TEST_NETID) + + private inner class TestNetwork(netId: Int) : Network(netId) { + override fun openConnection(url: URL): URLConnection { + val response = InstrumentationConnector.processRequest(url) + val responseBytes = response.content.toByteArray(StandardCharsets.UTF_8) + + val connection = mock(HttpURLConnection::class.java) + doReturn(response.responseCode).`when`(connection).responseCode + doReturn(responseBytes.size.toLong()).`when`(connection).contentLengthLong + doReturn(response.redirectUrl).`when`(connection).getHeaderField("location") + doReturn(ByteArrayInputStream(responseBytes)).`when`(connection).inputStream + return connection + } + } + + override fun makeNetworkMonitor( + network: Network, + name: String?, + cb: INetworkMonitorCallbacks + ) { + val nm = NetworkMonitor(this@TestNetworkStackService, cb, + this.network, + mock(IpConnectivityLog::class.java), mock(SharedLog::class.java), + mock(NetworkStackService.NetworkStackServiceManager::class.java), + NetworkMonitorDeps(privateDnsBypassNetwork), + mock(TcpSocketTracker::class.java)) + cb.onNetworkMonitorCreated(NetworkMonitorConnector(nm, TestPermissionChecker())) + } + } +} diff --git a/tests/integration/util/com/android/server/ConnectivityServiceTestUtils.kt b/tests/integration/util/com/android/server/ConnectivityServiceTestUtils.kt new file mode 100644 index 0000000000..165fd37282 --- /dev/null +++ b/tests/integration/util/com/android/server/ConnectivityServiceTestUtils.kt @@ -0,0 +1,43 @@ +/* + * 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 + */ + +@file:JvmName("ConnectivityServiceTestUtils") + +package com.android.server + +import android.net.ConnectivityManager.TYPE_BLUETOOTH +import android.net.ConnectivityManager.TYPE_ETHERNET +import android.net.ConnectivityManager.TYPE_MOBILE +import android.net.ConnectivityManager.TYPE_NONE +import android.net.ConnectivityManager.TYPE_TEST +import android.net.ConnectivityManager.TYPE_VPN +import android.net.ConnectivityManager.TYPE_WIFI +import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH +import android.net.NetworkCapabilities.TRANSPORT_CELLULAR +import android.net.NetworkCapabilities.TRANSPORT_ETHERNET +import android.net.NetworkCapabilities.TRANSPORT_TEST +import android.net.NetworkCapabilities.TRANSPORT_VPN +import android.net.NetworkCapabilities.TRANSPORT_WIFI + +fun transportToLegacyType(transport: Int) = when (transport) { + TRANSPORT_BLUETOOTH -> TYPE_BLUETOOTH + TRANSPORT_CELLULAR -> TYPE_MOBILE + TRANSPORT_ETHERNET -> TYPE_ETHERNET + TRANSPORT_TEST -> TYPE_TEST + TRANSPORT_VPN -> TYPE_VPN + TRANSPORT_WIFI -> TYPE_WIFI + else -> TYPE_NONE +} \ No newline at end of file diff --git a/tests/integration/util/com/android/server/NetworkAgentWrapper.java b/tests/integration/util/com/android/server/NetworkAgentWrapper.java new file mode 100644 index 0000000000..e80955014f --- /dev/null +++ b/tests/integration/util/com/android/server/NetworkAgentWrapper.java @@ -0,0 +1,377 @@ +/* + * 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 com.android.server; + +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; +import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE; + +import static com.android.server.ConnectivityServiceTestUtils.transportToLegacyType; + +import static junit.framework.Assert.assertTrue; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import android.annotation.NonNull; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkAgent; +import android.net.NetworkAgentConfig; +import android.net.NetworkCapabilities; +import android.net.NetworkProvider; +import android.net.NetworkSpecifier; +import android.net.QosFilter; +import android.net.SocketKeepalive; +import android.os.ConditionVariable; +import android.os.HandlerThread; +import android.os.Message; +import android.util.Log; +import android.util.Range; + +import com.android.net.module.util.ArrayTrackRecord; +import com.android.server.connectivity.ConnectivityConstants; +import com.android.testutils.HandlerUtils; +import com.android.testutils.TestableNetworkCallback; + +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +public class NetworkAgentWrapper implements TestableNetworkCallback.HasNetwork { + private final NetworkCapabilities mNetworkCapabilities; + private final HandlerThread mHandlerThread; + private final Context mContext; + private final String mLogTag; + private final NetworkAgentConfig mNetworkAgentConfig; + + private final ConditionVariable mDisconnected = new ConditionVariable(); + private final ConditionVariable mPreventReconnectReceived = new ConditionVariable(); + private final AtomicBoolean mConnected = new AtomicBoolean(false); + private int mScore; + private NetworkAgent mNetworkAgent; + private int mStartKeepaliveError = SocketKeepalive.ERROR_UNSUPPORTED; + private int mStopKeepaliveError = SocketKeepalive.NO_KEEPALIVE; + // Controls how test network agent is going to wait before responding to keepalive + // start/stop. Useful when simulate KeepaliveTracker is waiting for response from modem. + private long mKeepaliveResponseDelay = 0L; + private Integer mExpectedKeepaliveSlot = null; + private final ArrayTrackRecord.ReadHead mCallbackHistory = + new ArrayTrackRecord().newReadHead(); + + public NetworkAgentWrapper(int transport, LinkProperties linkProperties, + NetworkCapabilities ncTemplate, Context context) throws Exception { + final int type = transportToLegacyType(transport); + final String typeName = ConnectivityManager.getNetworkTypeName(type); + mNetworkCapabilities = (ncTemplate != null) ? ncTemplate : new NetworkCapabilities(); + mNetworkCapabilities.addCapability(NET_CAPABILITY_NOT_SUSPENDED); + mNetworkCapabilities.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED); + mNetworkCapabilities.addTransportType(transport); + switch (transport) { + case TRANSPORT_ETHERNET: + mScore = 70; + break; + case TRANSPORT_WIFI: + mScore = 60; + break; + case TRANSPORT_CELLULAR: + mScore = 50; + break; + case TRANSPORT_WIFI_AWARE: + mScore = 20; + break; + case TRANSPORT_VPN: + mNetworkCapabilities.removeCapability(NET_CAPABILITY_NOT_VPN); + // VPNs deduce the SUSPENDED capability from their underlying networks and there + // is no public API to let VPN services set it. + mNetworkCapabilities.removeCapability(NET_CAPABILITY_NOT_SUSPENDED); + mScore = ConnectivityConstants.VPN_DEFAULT_SCORE; + break; + default: + throw new UnsupportedOperationException("unimplemented network type"); + } + mContext = context; + mLogTag = "Mock-" + typeName; + mHandlerThread = new HandlerThread(mLogTag); + mHandlerThread.start(); + + // extraInfo is set to "" by default in NetworkAgentConfig. + final String extraInfo = (transport == TRANSPORT_CELLULAR) ? "internet.apn" : ""; + mNetworkAgentConfig = new NetworkAgentConfig.Builder() + .setLegacyType(type) + .setLegacyTypeName(typeName) + .setLegacyExtraInfo(extraInfo) + .build(); + mNetworkAgent = makeNetworkAgent(linkProperties, mNetworkAgentConfig); + } + + protected InstrumentedNetworkAgent makeNetworkAgent(LinkProperties linkProperties, + final NetworkAgentConfig nac) throws Exception { + return new InstrumentedNetworkAgent(this, linkProperties, nac); + } + + public static class InstrumentedNetworkAgent extends NetworkAgent { + private final NetworkAgentWrapper mWrapper; + private static final String PROVIDER_NAME = "InstrumentedNetworkAgentProvider"; + + public InstrumentedNetworkAgent(NetworkAgentWrapper wrapper, LinkProperties lp, + NetworkAgentConfig nac) { + super(wrapper.mContext, wrapper.mHandlerThread.getLooper(), wrapper.mLogTag, + wrapper.mNetworkCapabilities, lp, wrapper.mScore, nac, + new NetworkProvider(wrapper.mContext, wrapper.mHandlerThread.getLooper(), + PROVIDER_NAME)); + mWrapper = wrapper; + register(); + } + + @Override + public void unwanted() { + mWrapper.mDisconnected.open(); + } + + @Override + public void startSocketKeepalive(Message msg) { + int slot = msg.arg1; + if (mWrapper.mExpectedKeepaliveSlot != null) { + assertEquals((int) mWrapper.mExpectedKeepaliveSlot, slot); + } + mWrapper.mHandlerThread.getThreadHandler().postDelayed( + () -> onSocketKeepaliveEvent(slot, mWrapper.mStartKeepaliveError), + mWrapper.mKeepaliveResponseDelay); + } + + @Override + public void stopSocketKeepalive(Message msg) { + final int slot = msg.arg1; + mWrapper.mHandlerThread.getThreadHandler().postDelayed( + () -> onSocketKeepaliveEvent(slot, mWrapper.mStopKeepaliveError), + mWrapper.mKeepaliveResponseDelay); + } + + @Override + public void onQosCallbackRegistered(final int qosCallbackId, + final @NonNull QosFilter filter) { + Log.i(mWrapper.mLogTag, "onQosCallbackRegistered"); + mWrapper.mCallbackHistory.add( + new CallbackType.OnQosCallbackRegister(qosCallbackId, filter)); + } + + @Override + public void onQosCallbackUnregistered(final int qosCallbackId) { + Log.i(mWrapper.mLogTag, "onQosCallbackUnregistered"); + mWrapper.mCallbackHistory.add(new CallbackType.OnQosCallbackUnregister(qosCallbackId)); + } + + @Override + protected void preventAutomaticReconnect() { + mWrapper.mPreventReconnectReceived.open(); + } + + @Override + protected void addKeepalivePacketFilter(Message msg) { + Log.i(mWrapper.mLogTag, "Add keepalive packet filter."); + } + + @Override + protected void removeKeepalivePacketFilter(Message msg) { + Log.i(mWrapper.mLogTag, "Remove keepalive packet filter."); + } + } + + public void adjustScore(int change) { + mScore += change; + mNetworkAgent.sendNetworkScore(mScore); + } + + public int getScore() { + return mScore; + } + + public void explicitlySelected(boolean explicitlySelected, boolean acceptUnvalidated) { + mNetworkAgent.explicitlySelected(explicitlySelected, acceptUnvalidated); + } + + public void addCapability(int capability) { + mNetworkCapabilities.addCapability(capability); + mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities); + } + + public void removeCapability(int capability) { + mNetworkCapabilities.removeCapability(capability); + mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities); + } + + public void setUids(Set> uids) { + mNetworkCapabilities.setUids(uids); + mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities); + } + + public void setSignalStrength(int signalStrength) { + mNetworkCapabilities.setSignalStrength(signalStrength); + mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities); + } + + public void setNetworkSpecifier(NetworkSpecifier networkSpecifier) { + mNetworkCapabilities.setNetworkSpecifier(networkSpecifier); + mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities); + } + + public void setNetworkCapabilities(NetworkCapabilities nc, boolean sendToConnectivityService) { + mNetworkCapabilities.set(nc); + if (sendToConnectivityService) { + mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities); + } + } + + public void connect() { + if (!mConnected.compareAndSet(false /* expect */, true /* update */)) { + // compareAndSet returns false when the value couldn't be updated because it did not + // match the expected value. + fail("Test NetworkAgents can only be connected once"); + } + mNetworkAgent.markConnected(); + } + + public void suspend() { + removeCapability(NET_CAPABILITY_NOT_SUSPENDED); + } + + public void resume() { + addCapability(NET_CAPABILITY_NOT_SUSPENDED); + } + + public void disconnect() { + mNetworkAgent.unregister(); + } + + @Override + public Network getNetwork() { + return mNetworkAgent.getNetwork(); + } + + public void expectPreventReconnectReceived(long timeoutMs) { + assertTrue(mPreventReconnectReceived.block(timeoutMs)); + } + + public void expectDisconnected(long timeoutMs) { + assertTrue(mDisconnected.block(timeoutMs)); + } + + public void sendLinkProperties(LinkProperties lp) { + mNetworkAgent.sendLinkProperties(lp); + } + + public void setStartKeepaliveEvent(int reason) { + mStartKeepaliveError = reason; + } + + public void setStopKeepaliveEvent(int reason) { + mStopKeepaliveError = reason; + } + + public void setKeepaliveResponseDelay(long delay) { + mKeepaliveResponseDelay = delay; + } + + public void setExpectedKeepaliveSlot(Integer slot) { + mExpectedKeepaliveSlot = slot; + } + + public NetworkAgent getNetworkAgent() { + return mNetworkAgent; + } + + public NetworkCapabilities getNetworkCapabilities() { + return mNetworkCapabilities; + } + + public int getLegacyType() { + return mNetworkAgentConfig.getLegacyType(); + } + + public String getExtraInfo() { + return mNetworkAgentConfig.getLegacyExtraInfo(); + } + + public @NonNull ArrayTrackRecord.ReadHead getCallbackHistory() { + return mCallbackHistory; + } + + public void waitForIdle(long timeoutMs) { + HandlerUtils.waitForIdle(mHandlerThread, timeoutMs); + } + + abstract static class CallbackType { + final int mQosCallbackId; + + protected CallbackType(final int qosCallbackId) { + mQosCallbackId = qosCallbackId; + } + + static class OnQosCallbackRegister extends CallbackType { + final QosFilter mFilter; + OnQosCallbackRegister(final int qosCallbackId, final QosFilter filter) { + super(qosCallbackId); + mFilter = filter; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final OnQosCallbackRegister that = (OnQosCallbackRegister) o; + return mQosCallbackId == that.mQosCallbackId + && Objects.equals(mFilter, that.mFilter); + } + + @Override + public int hashCode() { + return Objects.hash(mQosCallbackId, mFilter); + } + } + + static class OnQosCallbackUnregister extends CallbackType { + OnQosCallbackUnregister(final int qosCallbackId) { + super(qosCallbackId); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final OnQosCallbackUnregister that = (OnQosCallbackUnregister) o; + return mQosCallbackId == that.mQosCallbackId; + } + + @Override + public int hashCode() { + return Objects.hash(mQosCallbackId); + } + } + } + + public boolean isBypassableVpn() { + return mNetworkAgentConfig.isBypassableVpn(); + } +} diff --git a/tests/integration/util/com/android/server/TestNetIdManager.kt b/tests/integration/util/com/android/server/TestNetIdManager.kt new file mode 100644 index 0000000000..938a694e8b --- /dev/null +++ b/tests/integration/util/com/android/server/TestNetIdManager.kt @@ -0,0 +1,39 @@ +/* + * 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 com.android.server + +import java.util.concurrent.atomic.AtomicInteger + +/** + * A [NetIdManager] that generates ID starting from [NetIdManager.MAX_NET_ID] and decreasing, rather + * than starting from [NetIdManager.MIN_NET_ID] and increasing. + * + * Useful for testing ConnectivityService, to minimize the risk of test ConnectivityService netIDs + * overlapping with netIDs used by the real ConnectivityService on the device. + * + * IDs may still overlap if many networks have been used on the device (so the "real" netIDs + * are close to MAX_NET_ID), but this is typically not the case when running unit tests. Also, there + * is no way to fully solve the overlap issue without altering ID allocation in non-test code, as + * the real ConnectivityService could start using netIds that have been used by the test in the + * past. + */ +class TestNetIdManager : NetIdManager() { + private val nextId = AtomicInteger(MAX_NET_ID) + override fun reserveNetId() = nextId.decrementAndGet() + override fun releaseNetId(id: Int) = Unit + fun peekNextNetId() = nextId.get() - 1 +} diff --git a/tests/smoketest/Android.bp b/tests/smoketest/Android.bp new file mode 100644 index 0000000000..1535f3ddcb --- /dev/null +++ b/tests/smoketest/Android.bp @@ -0,0 +1,31 @@ +// This test exists only because the jni_libs list for these tests is difficult to +// maintain: the test itself only depends on libnetworkstatsfactorytestjni, but the test +// fails to load that library unless *all* the dependencies of that library are explicitly +// listed in jni_libs. This means that whenever any of the dependencies changes the test +// starts failing and breaking presubmits in frameworks/base. We cannot easily put +// FrameworksNetTests into global presubmit because they are at times flaky, but this +// test is effectively empty beyond validating that the libraries load correctly, and +// thus should be stable enough to put in global presubmit. +// +// TODO: remove this hack when there is a better solution for jni_libs that includes +// dependent libraries. +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "FrameworksNetSmokeTests", + defaults: ["FrameworksNetTests-jni-defaults"], + srcs: ["java/SmokeTest.java"], + test_suites: ["device-tests"], + static_libs: [ + "androidx.test.rules", + "mockito-target-minus-junit4", + "services.core", + ], +} diff --git a/tests/smoketest/AndroidManifest.xml b/tests/smoketest/AndroidManifest.xml new file mode 100644 index 0000000000..f1b9febb9f --- /dev/null +++ b/tests/smoketest/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/tests/smoketest/AndroidTest.xml b/tests/smoketest/AndroidTest.xml new file mode 100644 index 0000000000..ac366e4ac5 --- /dev/null +++ b/tests/smoketest/AndroidTest.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/tests/smoketest/java/SmokeTest.java b/tests/smoketest/java/SmokeTest.java new file mode 100644 index 0000000000..7d6655fde1 --- /dev/null +++ b/tests/smoketest/java/SmokeTest.java @@ -0,0 +1,33 @@ +/* + * 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 com.android.server.net; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class SmokeTest { + + @Test + public void testLoadJni() { + System.loadLibrary("networkstatsfactorytestjni"); + assertEquals(0, 0x00); + } +} diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp new file mode 100644 index 0000000000..e1a424f214 --- /dev/null +++ b/tests/unit/Android.bp @@ -0,0 +1,87 @@ +//######################################################################## +// Build FrameworksNetTests package +//######################################################################## +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +java_defaults { + name: "FrameworksNetTests-jni-defaults", + jni_libs: [ + "ld-android", + "libbacktrace", + "libbase", + "libbinder", + "libbpf", + "libbpf_android", + "libc++", + "libcgrouprc", + "libcrypto", + "libcutils", + "libdl_android", + "libhidl-gen-utils", + "libhidlbase", + "libjsoncpp", + "liblog", + "liblzma", + "libnativehelper", + "libnetdbpf", + "libnetdutils", + "libnetworkstatsfactorytestjni", + "libpackagelistparser", + "libpcre2", + "libprocessgroup", + "libselinux", + "libtinyxml2", + "libui", + "libunwindstack", + "libutils", + "libutilscallstack", + "libvndksupport", + "libziparchive", + "libz", + "netd_aidl_interface-V5-cpp", + ], +} + +android_test { + name: "FrameworksNetTests", + defaults: ["FrameworksNetTests-jni-defaults"], + srcs: [ + "java/**/*.java", + "java/**/*.kt", + ], + platform_apis: true, + test_suites: ["device-tests"], + certificate: "platform", + jarjar_rules: "jarjar-rules.txt", + static_libs: [ + "androidx.test.rules", + "bouncycastle-repackaged-unbundled", + "FrameworksNetCommonTests", + "frameworks-base-testutils", + "frameworks-net-integration-testutils", + "framework-protos", + "mockito-target-minus-junit4", + "net-tests-utils", + "platform-test-annotations", + "service-connectivity-pre-jarjar", + "services.core", + "services.net", + ], + libs: [ + "android.net.ipsec.ike.stubs.module_lib", + "android.test.runner", + "android.test.base", + "android.test.mock", + "ServiceConnectivityResources", + ], + jni_libs: [ + "libservice-connectivity", + ], +} diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml new file mode 100644 index 0000000000..d08b2f8d40 --- /dev/null +++ b/tests/unit/AndroidManifest.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/unit/AndroidTest.xml b/tests/unit/AndroidTest.xml new file mode 100644 index 0000000000..939ae493b2 --- /dev/null +++ b/tests/unit/AndroidTest.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/tests/unit/jarjar-rules.txt b/tests/unit/jarjar-rules.txt new file mode 100644 index 0000000000..ca8867206d --- /dev/null +++ b/tests/unit/jarjar-rules.txt @@ -0,0 +1,2 @@ +# Module library in frameworks/libs/net +rule com.android.net.module.util.** android.net.frameworktests.util.@1 diff --git a/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java new file mode 100644 index 0000000000..899295a019 --- /dev/null +++ b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java @@ -0,0 +1,212 @@ +/* + * 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.app.usage; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.net.ConnectivityManager; +import android.net.INetworkStatsService; +import android.net.INetworkStatsSession; +import android.net.NetworkStats.Entry; +import android.net.NetworkStatsHistory; +import android.net.NetworkTemplate; +import android.os.RemoteException; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.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 org.mockito.invocation.InvocationOnMock; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkStatsManagerTest { + + private @Mock INetworkStatsService mService; + private @Mock INetworkStatsSession mStatsSession; + + private NetworkStatsManager mManager; + + // TODO: change to NetworkTemplate.MATCH_MOBILE once internal constant rename is merged to aosp. + private static final int MATCH_MOBILE_ALL = 1; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mManager = new NetworkStatsManager(InstrumentationRegistry.getContext(), mService); + } + + @Test + public void testQueryDetails() throws RemoteException { + final String subscriberId = "subid"; + final long startTime = 1; + final long endTime = 100; + final int uid1 = 10001; + final int uid2 = 10002; + final int uid3 = 10003; + + Entry uid1Entry1 = new Entry("if1", uid1, + android.net.NetworkStats.SET_DEFAULT, android.net.NetworkStats.TAG_NONE, + 100, 10, 200, 20, 0); + + Entry uid1Entry2 = new Entry( + "if2", uid1, + android.net.NetworkStats.SET_DEFAULT, android.net.NetworkStats.TAG_NONE, + 100, 10, 200, 20, 0); + + Entry uid2Entry1 = new Entry("if1", uid2, + android.net.NetworkStats.SET_DEFAULT, android.net.NetworkStats.TAG_NONE, + 150, 10, 250, 20, 0); + + Entry uid2Entry2 = new Entry( + "if2", uid2, + android.net.NetworkStats.SET_DEFAULT, android.net.NetworkStats.TAG_NONE, + 150, 10, 250, 20, 0); + + NetworkStatsHistory history1 = new NetworkStatsHistory(10, 2); + history1.recordData(10, 20, uid1Entry1); + history1.recordData(20, 30, uid1Entry2); + + NetworkStatsHistory history2 = new NetworkStatsHistory(10, 2); + history1.recordData(30, 40, uid2Entry1); + history1.recordData(35, 45, uid2Entry2); + + + when(mService.openSessionForUsageStats(anyInt(), anyString())).thenReturn(mStatsSession); + when(mStatsSession.getRelevantUids()).thenReturn(new int[] { uid1, uid2, uid3 }); + + when(mStatsSession.getHistoryIntervalForUid(any(NetworkTemplate.class), + eq(uid1), eq(android.net.NetworkStats.SET_ALL), + eq(android.net.NetworkStats.TAG_NONE), + eq(NetworkStatsHistory.FIELD_ALL), eq(startTime), eq(endTime))) + .then((InvocationOnMock inv) -> { + NetworkTemplate template = inv.getArgument(0); + assertEquals(MATCH_MOBILE_ALL, template.getMatchRule()); + assertEquals(subscriberId, template.getSubscriberId()); + return history1; + }); + + when(mStatsSession.getHistoryIntervalForUid(any(NetworkTemplate.class), + eq(uid2), eq(android.net.NetworkStats.SET_ALL), + eq(android.net.NetworkStats.TAG_NONE), + eq(NetworkStatsHistory.FIELD_ALL), eq(startTime), eq(endTime))) + .then((InvocationOnMock inv) -> { + NetworkTemplate template = inv.getArgument(0); + assertEquals(MATCH_MOBILE_ALL, template.getMatchRule()); + assertEquals(subscriberId, template.getSubscriberId()); + return history2; + }); + + + NetworkStats stats = mManager.queryDetails( + ConnectivityManager.TYPE_MOBILE, subscriberId, startTime, endTime); + + NetworkStats.Bucket bucket = new NetworkStats.Bucket(); + + // First 2 buckets exactly match entry timings + assertTrue(stats.getNextBucket(bucket)); + assertEquals(10, bucket.getStartTimeStamp()); + assertEquals(20, bucket.getEndTimeStamp()); + assertBucketMatches(uid1Entry1, bucket); + + assertTrue(stats.getNextBucket(bucket)); + assertEquals(20, bucket.getStartTimeStamp()); + assertEquals(30, bucket.getEndTimeStamp()); + assertBucketMatches(uid1Entry2, bucket); + + // 30 -> 40: contains uid2Entry1 and half of uid2Entry2 + assertTrue(stats.getNextBucket(bucket)); + assertEquals(30, bucket.getStartTimeStamp()); + assertEquals(40, bucket.getEndTimeStamp()); + assertEquals(225, bucket.getRxBytes()); + assertEquals(15, bucket.getRxPackets()); + assertEquals(375, bucket.getTxBytes()); + assertEquals(30, bucket.getTxPackets()); + + // 40 -> 50: contains half of uid2Entry2 + assertTrue(stats.getNextBucket(bucket)); + assertEquals(40, bucket.getStartTimeStamp()); + assertEquals(50, bucket.getEndTimeStamp()); + assertEquals(75, bucket.getRxBytes()); + assertEquals(5, bucket.getRxPackets()); + assertEquals(125, bucket.getTxBytes()); + assertEquals(10, bucket.getTxPackets()); + + assertFalse(stats.hasNextBucket()); + } + + @Test + public void testQueryDetails_NoSubscriberId() throws RemoteException { + final long startTime = 1; + final long endTime = 100; + final int uid1 = 10001; + final int uid2 = 10002; + + when(mService.openSessionForUsageStats(anyInt(), anyString())).thenReturn(mStatsSession); + when(mStatsSession.getRelevantUids()).thenReturn(new int[] { uid1, uid2 }); + + NetworkStats stats = mManager.queryDetails( + ConnectivityManager.TYPE_MOBILE, null, startTime, endTime); + + when(mStatsSession.getHistoryIntervalForUid(any(NetworkTemplate.class), + anyInt(), anyInt(), anyInt(), anyInt(), anyLong(), anyLong())) + .thenReturn(new NetworkStatsHistory(10, 0)); + + verify(mStatsSession, times(1)).getHistoryIntervalForUid( + argThat((NetworkTemplate t) -> + // No subscriberId: MATCH_MOBILE_WILDCARD template + t.getMatchRule() == NetworkTemplate.MATCH_MOBILE_WILDCARD), + eq(uid1), eq(android.net.NetworkStats.SET_ALL), + eq(android.net.NetworkStats.TAG_NONE), + eq(NetworkStatsHistory.FIELD_ALL), eq(startTime), eq(endTime)); + + verify(mStatsSession, times(1)).getHistoryIntervalForUid( + argThat((NetworkTemplate t) -> + // No subscriberId: MATCH_MOBILE_WILDCARD template + t.getMatchRule() == NetworkTemplate.MATCH_MOBILE_WILDCARD), + eq(uid2), eq(android.net.NetworkStats.SET_ALL), + eq(android.net.NetworkStats.TAG_NONE), + eq(NetworkStatsHistory.FIELD_ALL), eq(startTime), eq(endTime)); + + assertFalse(stats.hasNextBucket()); + } + + private void assertBucketMatches(Entry expected, NetworkStats.Bucket actual) { + assertEquals(expected.uid, actual.getUid()); + assertEquals(expected.rxBytes, actual.getRxBytes()); + assertEquals(expected.rxPackets, actual.getRxPackets()); + assertEquals(expected.txBytes, actual.getTxBytes()); + assertEquals(expected.txPackets, actual.getTxPackets()); + } +} diff --git a/tests/unit/java/android/net/ConnectivityDiagnosticsManagerTest.java b/tests/unit/java/android/net/ConnectivityDiagnosticsManagerTest.java new file mode 100644 index 0000000000..06e9405a6a --- /dev/null +++ b/tests/unit/java/android/net/ConnectivityDiagnosticsManagerTest.java @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2020 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; + +import static android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsBinder; +import static android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback; +import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport; +import static android.net.ConnectivityDiagnosticsManager.DataStallReport; + +import static com.android.testutils.ParcelUtils.assertParcelSane; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import android.content.Context; +import android.os.PersistableBundle; + +import androidx.test.InstrumentationRegistry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; + +import java.util.concurrent.Executor; + +@RunWith(JUnit4.class) +public class ConnectivityDiagnosticsManagerTest { + private static final int NET_ID = 1; + private static final int DETECTION_METHOD = 2; + private static final long TIMESTAMP = 10L; + private static final String INTERFACE_NAME = "interface"; + private static final String BUNDLE_KEY = "key"; + private static final String BUNDLE_VALUE = "value"; + + private static final Executor INLINE_EXECUTOR = x -> x.run(); + + @Mock private IConnectivityManager mService; + @Mock private ConnectivityDiagnosticsCallback mCb; + + private Context mContext; + private ConnectivityDiagnosticsBinder mBinder; + private ConnectivityDiagnosticsManager mManager; + + private String mPackageName; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getContext(); + + mService = mock(IConnectivityManager.class); + mCb = mock(ConnectivityDiagnosticsCallback.class); + + mBinder = new ConnectivityDiagnosticsBinder(mCb, INLINE_EXECUTOR); + mManager = new ConnectivityDiagnosticsManager(mContext, mService); + + mPackageName = mContext.getOpPackageName(); + } + + @After + public void tearDown() { + // clear ConnectivityDiagnosticsManager callbacks map + ConnectivityDiagnosticsManager.sCallbacks.clear(); + } + + private ConnectivityReport createSampleConnectivityReport() { + final LinkProperties linkProperties = new LinkProperties(); + linkProperties.setInterfaceName(INTERFACE_NAME); + + final NetworkCapabilities networkCapabilities = new NetworkCapabilities(); + networkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_IMS); + + final PersistableBundle bundle = new PersistableBundle(); + bundle.putString(BUNDLE_KEY, BUNDLE_VALUE); + + return new ConnectivityReport( + new Network(NET_ID), TIMESTAMP, linkProperties, networkCapabilities, bundle); + } + + private ConnectivityReport createDefaultConnectivityReport() { + return new ConnectivityReport( + new Network(0), + 0L, + new LinkProperties(), + new NetworkCapabilities(), + PersistableBundle.EMPTY); + } + + @Test + public void testPersistableBundleEquals() { + assertFalse( + ConnectivityDiagnosticsManager.persistableBundleEquals( + null, PersistableBundle.EMPTY)); + assertFalse( + ConnectivityDiagnosticsManager.persistableBundleEquals( + PersistableBundle.EMPTY, null)); + assertTrue( + ConnectivityDiagnosticsManager.persistableBundleEquals( + PersistableBundle.EMPTY, PersistableBundle.EMPTY)); + + final PersistableBundle a = new PersistableBundle(); + a.putString(BUNDLE_KEY, BUNDLE_VALUE); + + final PersistableBundle b = new PersistableBundle(); + b.putString(BUNDLE_KEY, BUNDLE_VALUE); + + final PersistableBundle c = new PersistableBundle(); + c.putString(BUNDLE_KEY, null); + + assertFalse( + ConnectivityDiagnosticsManager.persistableBundleEquals(PersistableBundle.EMPTY, a)); + assertFalse( + ConnectivityDiagnosticsManager.persistableBundleEquals(a, PersistableBundle.EMPTY)); + + assertTrue(ConnectivityDiagnosticsManager.persistableBundleEquals(a, b)); + assertTrue(ConnectivityDiagnosticsManager.persistableBundleEquals(b, a)); + + assertFalse(ConnectivityDiagnosticsManager.persistableBundleEquals(a, c)); + assertFalse(ConnectivityDiagnosticsManager.persistableBundleEquals(c, a)); + } + + @Test + public void testConnectivityReportEquals() { + final ConnectivityReport defaultReport = createDefaultConnectivityReport(); + final ConnectivityReport sampleReport = createSampleConnectivityReport(); + assertEquals(sampleReport, createSampleConnectivityReport()); + assertEquals(defaultReport, createDefaultConnectivityReport()); + + final LinkProperties linkProperties = sampleReport.getLinkProperties(); + final NetworkCapabilities networkCapabilities = sampleReport.getNetworkCapabilities(); + final PersistableBundle bundle = sampleReport.getAdditionalInfo(); + + assertNotEquals( + createDefaultConnectivityReport(), + new ConnectivityReport( + new Network(NET_ID), + 0L, + new LinkProperties(), + new NetworkCapabilities(), + PersistableBundle.EMPTY)); + assertNotEquals( + createDefaultConnectivityReport(), + new ConnectivityReport( + new Network(0), + TIMESTAMP, + new LinkProperties(), + new NetworkCapabilities(), + PersistableBundle.EMPTY)); + assertNotEquals( + createDefaultConnectivityReport(), + new ConnectivityReport( + new Network(0), + 0L, + linkProperties, + new NetworkCapabilities(), + PersistableBundle.EMPTY)); + assertNotEquals( + createDefaultConnectivityReport(), + new ConnectivityReport( + new Network(0), + TIMESTAMP, + new LinkProperties(), + networkCapabilities, + PersistableBundle.EMPTY)); + assertNotEquals( + createDefaultConnectivityReport(), + new ConnectivityReport( + new Network(0), + TIMESTAMP, + new LinkProperties(), + new NetworkCapabilities(), + bundle)); + } + + @Test + public void testConnectivityReportParcelUnparcel() { + assertParcelSane(createSampleConnectivityReport(), 5); + } + + private DataStallReport createSampleDataStallReport() { + final LinkProperties linkProperties = new LinkProperties(); + linkProperties.setInterfaceName(INTERFACE_NAME); + + final PersistableBundle bundle = new PersistableBundle(); + bundle.putString(BUNDLE_KEY, BUNDLE_VALUE); + + final NetworkCapabilities networkCapabilities = new NetworkCapabilities(); + networkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_IMS); + + return new DataStallReport( + new Network(NET_ID), + TIMESTAMP, + DETECTION_METHOD, + linkProperties, + networkCapabilities, + bundle); + } + + private DataStallReport createDefaultDataStallReport() { + return new DataStallReport( + new Network(0), + 0L, + 0, + new LinkProperties(), + new NetworkCapabilities(), + PersistableBundle.EMPTY); + } + + @Test + public void testDataStallReportEquals() { + final DataStallReport defaultReport = createDefaultDataStallReport(); + final DataStallReport sampleReport = createSampleDataStallReport(); + assertEquals(sampleReport, createSampleDataStallReport()); + assertEquals(defaultReport, createDefaultDataStallReport()); + + final LinkProperties linkProperties = sampleReport.getLinkProperties(); + final NetworkCapabilities networkCapabilities = sampleReport.getNetworkCapabilities(); + final PersistableBundle bundle = sampleReport.getStallDetails(); + + assertNotEquals( + defaultReport, + new DataStallReport( + new Network(NET_ID), + 0L, + 0, + new LinkProperties(), + new NetworkCapabilities(), + PersistableBundle.EMPTY)); + assertNotEquals( + defaultReport, + new DataStallReport( + new Network(0), + TIMESTAMP, + 0, + new LinkProperties(), + new NetworkCapabilities(), + PersistableBundle.EMPTY)); + assertNotEquals( + defaultReport, + new DataStallReport( + new Network(0), + 0L, + DETECTION_METHOD, + new LinkProperties(), + new NetworkCapabilities(), + PersistableBundle.EMPTY)); + assertNotEquals( + defaultReport, + new DataStallReport( + new Network(0), + 0L, + 0, + linkProperties, + new NetworkCapabilities(), + PersistableBundle.EMPTY)); + assertNotEquals( + defaultReport, + new DataStallReport( + new Network(0), + 0L, + 0, + new LinkProperties(), + networkCapabilities, + PersistableBundle.EMPTY)); + assertNotEquals( + defaultReport, + new DataStallReport( + new Network(0), + 0L, + 0, + new LinkProperties(), + new NetworkCapabilities(), + bundle)); + } + + @Test + public void testDataStallReportParcelUnparcel() { + assertParcelSane(createSampleDataStallReport(), 6); + } + + @Test + public void testConnectivityDiagnosticsCallbackOnConnectivityReportAvailable() { + mBinder.onConnectivityReportAvailable(createSampleConnectivityReport()); + + // The callback will be invoked synchronously by inline executor. Immediately check the + // latch without waiting. + verify(mCb).onConnectivityReportAvailable(eq(createSampleConnectivityReport())); + } + + @Test + public void testConnectivityDiagnosticsCallbackOnDataStallSuspected() { + mBinder.onDataStallSuspected(createSampleDataStallReport()); + + // The callback will be invoked synchronously by inline executor. Immediately check the + // latch without waiting. + verify(mCb).onDataStallSuspected(eq(createSampleDataStallReport())); + } + + @Test + public void testConnectivityDiagnosticsCallbackOnNetworkConnectivityReported() { + final Network n = new Network(NET_ID); + final boolean connectivity = true; + + mBinder.onNetworkConnectivityReported(n, connectivity); + + // The callback will be invoked synchronously by inline executor. Immediately check the + // latch without waiting. + verify(mCb).onNetworkConnectivityReported(eq(n), eq(connectivity)); + } + + @Test + public void testRegisterConnectivityDiagnosticsCallback() throws Exception { + final NetworkRequest request = new NetworkRequest.Builder().build(); + + mManager.registerConnectivityDiagnosticsCallback(request, INLINE_EXECUTOR, mCb); + + verify(mService).registerConnectivityDiagnosticsCallback( + any(ConnectivityDiagnosticsBinder.class), eq(request), eq(mPackageName)); + assertTrue(ConnectivityDiagnosticsManager.sCallbacks.containsKey(mCb)); + } + + @Test + public void testRegisterDuplicateConnectivityDiagnosticsCallback() throws Exception { + final NetworkRequest request = new NetworkRequest.Builder().build(); + + mManager.registerConnectivityDiagnosticsCallback(request, INLINE_EXECUTOR, mCb); + + try { + mManager.registerConnectivityDiagnosticsCallback(request, INLINE_EXECUTOR, mCb); + fail("Duplicate callback registration should fail"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testUnregisterConnectivityDiagnosticsCallback() throws Exception { + final NetworkRequest request = new NetworkRequest.Builder().build(); + mManager.registerConnectivityDiagnosticsCallback(request, INLINE_EXECUTOR, mCb); + + mManager.unregisterConnectivityDiagnosticsCallback(mCb); + + verify(mService).unregisterConnectivityDiagnosticsCallback( + any(ConnectivityDiagnosticsBinder.class)); + assertFalse(ConnectivityDiagnosticsManager.sCallbacks.containsKey(mCb)); + + // verify that re-registering is successful + mManager.registerConnectivityDiagnosticsCallback(request, INLINE_EXECUTOR, mCb); + verify(mService, times(2)).registerConnectivityDiagnosticsCallback( + any(ConnectivityDiagnosticsBinder.class), eq(request), eq(mPackageName)); + assertTrue(ConnectivityDiagnosticsManager.sCallbacks.containsKey(mCb)); + } + + @Test + public void testUnregisterUnknownConnectivityDiagnosticsCallback() throws Exception { + mManager.unregisterConnectivityDiagnosticsCallback(mCb); + + verifyNoMoreInteractions(mService); + } +} diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java new file mode 100644 index 0000000000..591e0cc350 --- /dev/null +++ b/tests/unit/java/android/net/ConnectivityManagerTest.java @@ -0,0 +1,430 @@ +/* + * 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 android.net; + +import static android.net.ConnectivityManager.TYPE_NONE; +import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA; +import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_SUPL; +import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P; +import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.NetworkRequest.Type.BACKGROUND_REQUEST; +import static android.net.NetworkRequest.Type.REQUEST; +import static android.net.NetworkRequest.Type.TRACK_DEFAULT; +import static android.net.NetworkRequest.Type.TRACK_SYSTEM_DEFAULT; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +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.PendingIntent; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.net.ConnectivityManager.NetworkCallback; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.os.Process; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +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; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ConnectivityManagerTest { + + @Mock Context mCtx; + @Mock IConnectivityManager mService; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + static NetworkCapabilities verifyNetworkCapabilities( + int legacyType, int transportType, int... capabilities) { + final NetworkCapabilities nc = ConnectivityManager.networkCapabilitiesForType(legacyType); + assertNotNull(nc); + assertTrue(nc.hasTransport(transportType)); + for (int capability : capabilities) { + assertTrue(nc.hasCapability(capability)); + } + + return nc; + } + + static void verifyUnrestrictedNetworkCapabilities(int legacyType, int transportType) { + verifyNetworkCapabilities( + legacyType, + transportType, + NET_CAPABILITY_INTERNET, + NET_CAPABILITY_NOT_RESTRICTED, + NET_CAPABILITY_NOT_VPN, + NET_CAPABILITY_TRUSTED); + } + + static void verifyRestrictedMobileNetworkCapabilities(int legacyType, int capability) { + final NetworkCapabilities nc = verifyNetworkCapabilities( + legacyType, + TRANSPORT_CELLULAR, + capability, + NET_CAPABILITY_NOT_VPN, + NET_CAPABILITY_TRUSTED); + + assertFalse(nc.hasCapability(NET_CAPABILITY_INTERNET)); + assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + } + + @Test + public void testNetworkCapabilitiesForTypeMobile() { + verifyUnrestrictedNetworkCapabilities( + ConnectivityManager.TYPE_MOBILE, TRANSPORT_CELLULAR); + } + + @Test + public void testNetworkCapabilitiesForTypeMobileCbs() { + verifyRestrictedMobileNetworkCapabilities( + ConnectivityManager.TYPE_MOBILE_CBS, NET_CAPABILITY_CBS); + } + + @Test + public void testNetworkCapabilitiesForTypeMobileDun() { + verifyRestrictedMobileNetworkCapabilities( + ConnectivityManager.TYPE_MOBILE_DUN, NET_CAPABILITY_DUN); + } + + @Test + public void testNetworkCapabilitiesForTypeMobileFota() { + verifyRestrictedMobileNetworkCapabilities( + ConnectivityManager.TYPE_MOBILE_FOTA, NET_CAPABILITY_FOTA); + } + + @Test + public void testNetworkCapabilitiesForTypeMobileHipri() { + verifyUnrestrictedNetworkCapabilities( + ConnectivityManager.TYPE_MOBILE_HIPRI, TRANSPORT_CELLULAR); + } + + @Test + public void testNetworkCapabilitiesForTypeMobileIms() { + verifyRestrictedMobileNetworkCapabilities( + ConnectivityManager.TYPE_MOBILE_IMS, NET_CAPABILITY_IMS); + } + + @Test + public void testNetworkCapabilitiesForTypeMobileMms() { + final NetworkCapabilities nc = verifyNetworkCapabilities( + ConnectivityManager.TYPE_MOBILE_MMS, + TRANSPORT_CELLULAR, + NET_CAPABILITY_MMS, + NET_CAPABILITY_NOT_VPN, + NET_CAPABILITY_TRUSTED); + + assertFalse(nc.hasCapability(NET_CAPABILITY_INTERNET)); + } + + @Test + public void testNetworkCapabilitiesForTypeMobileSupl() { + final NetworkCapabilities nc = verifyNetworkCapabilities( + ConnectivityManager.TYPE_MOBILE_SUPL, + TRANSPORT_CELLULAR, + NET_CAPABILITY_SUPL, + NET_CAPABILITY_NOT_VPN, + NET_CAPABILITY_TRUSTED); + + assertFalse(nc.hasCapability(NET_CAPABILITY_INTERNET)); + } + + @Test + public void testNetworkCapabilitiesForTypeWifi() { + verifyUnrestrictedNetworkCapabilities( + ConnectivityManager.TYPE_WIFI, TRANSPORT_WIFI); + } + + @Test + public void testNetworkCapabilitiesForTypeWifiP2p() { + final NetworkCapabilities nc = verifyNetworkCapabilities( + ConnectivityManager.TYPE_WIFI_P2P, + TRANSPORT_WIFI, + NET_CAPABILITY_NOT_RESTRICTED, NET_CAPABILITY_NOT_VPN, + NET_CAPABILITY_TRUSTED, NET_CAPABILITY_WIFI_P2P); + + assertFalse(nc.hasCapability(NET_CAPABILITY_INTERNET)); + } + + @Test + public void testNetworkCapabilitiesForTypeBluetooth() { + verifyUnrestrictedNetworkCapabilities( + ConnectivityManager.TYPE_BLUETOOTH, TRANSPORT_BLUETOOTH); + } + + @Test + public void testNetworkCapabilitiesForTypeEthernet() { + verifyUnrestrictedNetworkCapabilities( + ConnectivityManager.TYPE_ETHERNET, TRANSPORT_ETHERNET); + } + + @Test + public void testCallbackRelease() throws Exception { + ConnectivityManager manager = new ConnectivityManager(mCtx, mService); + NetworkRequest request = makeRequest(1); + NetworkCallback callback = mock(ConnectivityManager.NetworkCallback.class); + Handler handler = new Handler(Looper.getMainLooper()); + ArgumentCaptor captor = ArgumentCaptor.forClass(Messenger.class); + + // register callback + when(mService.requestNetwork(anyInt(), any(), anyInt(), captor.capture(), anyInt(), any(), + anyInt(), anyInt(), any(), nullable(String.class))).thenReturn(request); + manager.requestNetwork(request, callback, handler); + + // callback triggers + captor.getValue().send(makeMessage(request, ConnectivityManager.CALLBACK_AVAILABLE)); + verify(callback, timeout(500).times(1)).onAvailable(any(Network.class), + any(NetworkCapabilities.class), any(LinkProperties.class), anyBoolean()); + + // unregister callback + manager.unregisterNetworkCallback(callback); + verify(mService, times(1)).releaseNetworkRequest(request); + + // callback does not trigger anymore. + captor.getValue().send(makeMessage(request, ConnectivityManager.CALLBACK_LOSING)); + verify(callback, timeout(500).times(0)).onLosing(any(), anyInt()); + } + + @Test + public void testCallbackRecycling() throws Exception { + ConnectivityManager manager = new ConnectivityManager(mCtx, mService); + NetworkRequest req1 = makeRequest(1); + NetworkRequest req2 = makeRequest(2); + NetworkCallback callback = mock(ConnectivityManager.NetworkCallback.class); + Handler handler = new Handler(Looper.getMainLooper()); + ArgumentCaptor captor = ArgumentCaptor.forClass(Messenger.class); + + // register callback + when(mService.requestNetwork(anyInt(), any(), anyInt(), captor.capture(), anyInt(), any(), + anyInt(), anyInt(), any(), nullable(String.class))).thenReturn(req1); + manager.requestNetwork(req1, callback, handler); + + // callback triggers + captor.getValue().send(makeMessage(req1, ConnectivityManager.CALLBACK_AVAILABLE)); + verify(callback, timeout(100).times(1)).onAvailable(any(Network.class), + any(NetworkCapabilities.class), any(LinkProperties.class), anyBoolean()); + + // unregister callback + manager.unregisterNetworkCallback(callback); + verify(mService, times(1)).releaseNetworkRequest(req1); + + // callback does not trigger anymore. + captor.getValue().send(makeMessage(req1, ConnectivityManager.CALLBACK_LOSING)); + verify(callback, timeout(100).times(0)).onLosing(any(), anyInt()); + + // callback can be registered again + when(mService.requestNetwork(anyInt(), any(), anyInt(), captor.capture(), anyInt(), any(), + anyInt(), anyInt(), any(), nullable(String.class))).thenReturn(req2); + manager.requestNetwork(req2, callback, handler); + + // callback triggers + captor.getValue().send(makeMessage(req2, ConnectivityManager.CALLBACK_LOST)); + verify(callback, timeout(100).times(1)).onLost(any()); + + // unregister callback + manager.unregisterNetworkCallback(callback); + verify(mService, times(1)).releaseNetworkRequest(req2); + } + + // TODO: turn on this test when request callback 1:1 mapping is enforced + //@Test + private void noDoubleCallbackRegistration() throws Exception { + ConnectivityManager manager = new ConnectivityManager(mCtx, mService); + NetworkRequest request = makeRequest(1); + NetworkCallback callback = new ConnectivityManager.NetworkCallback(); + ApplicationInfo info = new ApplicationInfo(); + // TODO: update version when starting to enforce 1:1 mapping + info.targetSdkVersion = VERSION_CODES.N_MR1 + 1; + + when(mCtx.getApplicationInfo()).thenReturn(info); + when(mService.requestNetwork(anyInt(), any(), anyInt(), any(), anyInt(), any(), anyInt(), + anyInt(), any(), nullable(String.class))).thenReturn(request); + + Handler handler = new Handler(Looper.getMainLooper()); + manager.requestNetwork(request, callback, handler); + + // callback is already registered, reregistration should fail. + Class wantException = IllegalArgumentException.class; + expectThrowable(() -> manager.requestNetwork(request, callback), wantException); + + manager.unregisterNetworkCallback(callback); + verify(mService, times(1)).releaseNetworkRequest(request); + + // unregistering the callback should make it registrable again. + manager.requestNetwork(request, callback); + } + + @Test + public void testArgumentValidation() throws Exception { + ConnectivityManager manager = new ConnectivityManager(mCtx, mService); + + NetworkRequest request = mock(NetworkRequest.class); + NetworkCallback callback = mock(NetworkCallback.class); + Handler handler = mock(Handler.class); + NetworkCallback nullCallback = null; + PendingIntent nullIntent = null; + + mustFail(() -> { manager.requestNetwork(null, callback); }); + mustFail(() -> { manager.requestNetwork(request, nullCallback); }); + mustFail(() -> { manager.requestNetwork(request, callback, null); }); + mustFail(() -> { manager.requestNetwork(request, callback, -1); }); + mustFail(() -> { manager.requestNetwork(request, nullIntent); }); + + mustFail(() -> { manager.registerNetworkCallback(null, callback, handler); }); + mustFail(() -> { manager.registerNetworkCallback(request, null, handler); }); + mustFail(() -> { manager.registerNetworkCallback(request, callback, null); }); + mustFail(() -> { manager.registerNetworkCallback(request, nullIntent); }); + + mustFail(() -> { manager.registerDefaultNetworkCallback(null, handler); }); + mustFail(() -> { manager.registerDefaultNetworkCallback(callback, null); }); + + mustFail(() -> { manager.registerSystemDefaultNetworkCallback(null, handler); }); + mustFail(() -> { manager.registerSystemDefaultNetworkCallback(callback, null); }); + + mustFail(() -> { manager.unregisterNetworkCallback(nullCallback); }); + mustFail(() -> { manager.unregisterNetworkCallback(nullIntent); }); + mustFail(() -> { manager.releaseNetworkRequest(nullIntent); }); + } + + static void mustFail(Runnable fn) { + try { + fn.run(); + fail(); + } catch (Exception expected) { + } + } + + @Test + public void testRequestType() throws Exception { + final String testPkgName = "MyPackage"; + final String testAttributionTag = "MyTag"; + final ConnectivityManager manager = new ConnectivityManager(mCtx, mService); + when(mCtx.getOpPackageName()).thenReturn(testPkgName); + when(mCtx.getAttributionTag()).thenReturn(testAttributionTag); + final NetworkRequest request = makeRequest(1); + final NetworkCallback callback = new ConnectivityManager.NetworkCallback(); + + manager.requestNetwork(request, callback); + verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(request.networkCapabilities), + eq(REQUEST.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(), + eq(testPkgName), eq(testAttributionTag)); + reset(mService); + + // Verify that register network callback does not calls requestNetwork at all. + manager.registerNetworkCallback(request, callback); + verify(mService, never()).requestNetwork(anyInt(), any(), anyInt(), any(), anyInt(), any(), + anyInt(), anyInt(), any(), any()); + verify(mService).listenForNetwork(eq(request.networkCapabilities), any(), any(), anyInt(), + eq(testPkgName), eq(testAttributionTag)); + reset(mService); + + Handler handler = new Handler(ConnectivityThread.getInstanceLooper()); + + manager.registerDefaultNetworkCallback(callback); + verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(null), + eq(TRACK_DEFAULT.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(), + eq(testPkgName), eq(testAttributionTag)); + reset(mService); + + manager.registerDefaultNetworkCallbackForUid(42, callback, handler); + verify(mService).requestNetwork(eq(42), eq(null), + eq(TRACK_DEFAULT.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(), + eq(testPkgName), eq(testAttributionTag)); + + manager.requestBackgroundNetwork(request, callback, handler); + verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(request.networkCapabilities), + eq(BACKGROUND_REQUEST.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(), + eq(testPkgName), eq(testAttributionTag)); + reset(mService); + + manager.registerSystemDefaultNetworkCallback(callback, handler); + verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(null), + eq(TRACK_SYSTEM_DEFAULT.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(), + eq(testPkgName), eq(testAttributionTag)); + reset(mService); + } + + static Message makeMessage(NetworkRequest req, int messageType) { + Bundle bundle = new Bundle(); + bundle.putParcelable(NetworkRequest.class.getSimpleName(), req); + // Pass default objects as we don't care which get passed here + bundle.putParcelable(Network.class.getSimpleName(), new Network(1)); + bundle.putParcelable(NetworkCapabilities.class.getSimpleName(), new NetworkCapabilities()); + bundle.putParcelable(LinkProperties.class.getSimpleName(), new LinkProperties()); + Message msg = Message.obtain(); + msg.what = messageType; + msg.setData(bundle); + return msg; + } + + static NetworkRequest makeRequest(int requestId) { + NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build(); + return new NetworkRequest(request.networkCapabilities, ConnectivityManager.TYPE_NONE, + requestId, NetworkRequest.Type.NONE); + } + + static void expectThrowable(Runnable block, Class throwableType) { + try { + block.run(); + } catch (Throwable t) { + if (t.getClass().equals(throwableType)) { + return; + } + fail("expected exception of type " + throwableType + ", but was " + t.getClass()); + } + fail("expected exception of type " + throwableType); + } +} diff --git a/tests/unit/java/android/net/Ikev2VpnProfileTest.java b/tests/unit/java/android/net/Ikev2VpnProfileTest.java new file mode 100644 index 0000000000..1abd39a32b --- /dev/null +++ b/tests/unit/java/android/net/Ikev2VpnProfileTest.java @@ -0,0 +1,439 @@ +/* + * 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; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.test.mock.MockContext; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.net.VpnProfile; +import com.android.net.module.util.ProxyUtils; +import com.android.internal.org.bouncycastle.x509.X509V1CertificateGenerator; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.security.auth.x500.X500Principal; + +/** Unit tests for {@link Ikev2VpnProfile.Builder}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class Ikev2VpnProfileTest { + private static final String SERVER_ADDR_STRING = "1.2.3.4"; + private static final String IDENTITY_STRING = "Identity"; + private static final String USERNAME_STRING = "username"; + private static final String PASSWORD_STRING = "pa55w0rd"; + private static final String EXCL_LIST = "exclList"; + private static final byte[] PSK_BYTES = "preSharedKey".getBytes(); + private static final int TEST_MTU = 1300; + + private final MockContext mMockContext = + new MockContext() { + @Override + public String getOpPackageName() { + return "fooPackage"; + } + }; + private final ProxyInfo mProxy = ProxyInfo.buildDirectProxy( + SERVER_ADDR_STRING, -1, ProxyUtils.exclusionStringAsList(EXCL_LIST)); + + private X509Certificate mUserCert; + private X509Certificate mServerRootCa; + private PrivateKey mPrivateKey; + + @Before + public void setUp() throws Exception { + mServerRootCa = generateRandomCertAndKeyPair().cert; + + final CertificateAndKey userCertKey = generateRandomCertAndKeyPair(); + mUserCert = userCertKey.cert; + mPrivateKey = userCertKey.key; + } + + private Ikev2VpnProfile.Builder getBuilderWithDefaultOptions() { + final Ikev2VpnProfile.Builder builder = + new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING); + + builder.setBypassable(true); + builder.setProxy(mProxy); + builder.setMaxMtu(TEST_MTU); + builder.setMetered(true); + + return builder; + } + + @Test + public void testBuildValidProfileWithOptions() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa); + final Ikev2VpnProfile profile = builder.build(); + assertNotNull(profile); + + // Check non-auth parameters correctly stored + assertEquals(SERVER_ADDR_STRING, profile.getServerAddr()); + assertEquals(IDENTITY_STRING, profile.getUserIdentity()); + assertEquals(mProxy, profile.getProxyInfo()); + assertTrue(profile.isBypassable()); + assertTrue(profile.isMetered()); + assertEquals(TEST_MTU, profile.getMaxMtu()); + assertEquals(Ikev2VpnProfile.DEFAULT_ALGORITHMS, profile.getAllowedAlgorithms()); + } + + @Test + public void testBuildUsernamePasswordProfile() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa); + final Ikev2VpnProfile profile = builder.build(); + assertNotNull(profile); + + assertEquals(USERNAME_STRING, profile.getUsername()); + assertEquals(PASSWORD_STRING, profile.getPassword()); + assertEquals(mServerRootCa, profile.getServerRootCaCert()); + + assertNull(profile.getPresharedKey()); + assertNull(profile.getRsaPrivateKey()); + assertNull(profile.getUserCert()); + } + + @Test + public void testBuildDigitalSignatureProfile() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa); + final Ikev2VpnProfile profile = builder.build(); + assertNotNull(profile); + + assertEquals(profile.getUserCert(), mUserCert); + assertEquals(mPrivateKey, profile.getRsaPrivateKey()); + assertEquals(profile.getServerRootCaCert(), mServerRootCa); + + assertNull(profile.getPresharedKey()); + assertNull(profile.getUsername()); + assertNull(profile.getPassword()); + } + + @Test + public void testBuildPresharedKeyProfile() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthPsk(PSK_BYTES); + final Ikev2VpnProfile profile = builder.build(); + assertNotNull(profile); + + assertArrayEquals(PSK_BYTES, profile.getPresharedKey()); + + assertNull(profile.getServerRootCaCert()); + assertNull(profile.getUsername()); + assertNull(profile.getPassword()); + assertNull(profile.getRsaPrivateKey()); + assertNull(profile.getUserCert()); + } + + @Test + public void testBuildWithAllowedAlgorithmsAead() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + builder.setAuthPsk(PSK_BYTES); + + List allowedAlgorithms = Arrays.asList(IpSecAlgorithm.AUTH_CRYPT_AES_GCM); + builder.setAllowedAlgorithms(allowedAlgorithms); + + final Ikev2VpnProfile profile = builder.build(); + assertEquals(allowedAlgorithms, profile.getAllowedAlgorithms()); + } + + @Test + public void testBuildWithAllowedAlgorithmsNormal() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + builder.setAuthPsk(PSK_BYTES); + + List allowedAlgorithms = + Arrays.asList(IpSecAlgorithm.AUTH_HMAC_SHA512, IpSecAlgorithm.CRYPT_AES_CBC); + builder.setAllowedAlgorithms(allowedAlgorithms); + + final Ikev2VpnProfile profile = builder.build(); + assertEquals(allowedAlgorithms, profile.getAllowedAlgorithms()); + } + + @Test + public void testSetAllowedAlgorithmsEmptyList() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + try { + builder.setAllowedAlgorithms(new ArrayList<>()); + fail("Expected exception due to no valid algorithm set"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testSetAllowedAlgorithmsInvalidList() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + List allowedAlgorithms = new ArrayList<>(); + + try { + builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_SHA256)); + fail("Expected exception due to missing encryption"); + } catch (IllegalArgumentException expected) { + } + + try { + builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.CRYPT_AES_CBC)); + fail("Expected exception due to missing authentication"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testSetAllowedAlgorithmsInsecureAlgorithm() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + List allowedAlgorithms = new ArrayList<>(); + + try { + builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_MD5)); + fail("Expected exception due to insecure algorithm"); + } catch (IllegalArgumentException expected) { + } + + try { + builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_SHA1)); + fail("Expected exception due to insecure algorithm"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testBuildNoAuthMethodSet() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + try { + builder.build(); + fail("Expected exception due to lack of auth method"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testBuildInvalidMtu() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + try { + builder.setMaxMtu(500); + fail("Expected exception due to too-small MTU"); + } catch (IllegalArgumentException expected) { + } + } + + private void verifyVpnProfileCommon(VpnProfile profile) { + assertEquals(SERVER_ADDR_STRING, profile.server); + assertEquals(IDENTITY_STRING, profile.ipsecIdentifier); + assertEquals(mProxy, profile.proxy); + assertTrue(profile.isBypassable); + assertTrue(profile.isMetered); + assertEquals(TEST_MTU, profile.maxMtu); + } + + @Test + public void testPskConvertToVpnProfile() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthPsk(PSK_BYTES); + final VpnProfile profile = builder.build().toVpnProfile(); + + verifyVpnProfileCommon(profile); + assertEquals(Ikev2VpnProfile.encodeForIpsecSecret(PSK_BYTES), profile.ipsecSecret); + + // Check nothing else is set + assertEquals("", profile.username); + assertEquals("", profile.password); + assertEquals("", profile.ipsecUserCert); + assertEquals("", profile.ipsecCaCert); + } + + @Test + public void testUsernamePasswordConvertToVpnProfile() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa); + final VpnProfile profile = builder.build().toVpnProfile(); + + verifyVpnProfileCommon(profile); + assertEquals(USERNAME_STRING, profile.username); + assertEquals(PASSWORD_STRING, profile.password); + assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert); + + // Check nothing else is set + assertEquals("", profile.ipsecUserCert); + assertEquals("", profile.ipsecSecret); + } + + @Test + public void testRsaConvertToVpnProfile() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa); + final VpnProfile profile = builder.build().toVpnProfile(); + + final String expectedSecret = Ikev2VpnProfile.PREFIX_INLINE + + Ikev2VpnProfile.encodeForIpsecSecret(mPrivateKey.getEncoded()); + verifyVpnProfileCommon(profile); + assertEquals(Ikev2VpnProfile.certificateToPemString(mUserCert), profile.ipsecUserCert); + assertEquals( + expectedSecret, + profile.ipsecSecret); + assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert); + + // Check nothing else is set + assertEquals("", profile.username); + assertEquals("", profile.password); + } + + @Test + public void testPskFromVpnProfileDiscardsIrrelevantValues() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthPsk(PSK_BYTES); + final VpnProfile profile = builder.build().toVpnProfile(); + profile.username = USERNAME_STRING; + profile.password = PASSWORD_STRING; + profile.ipsecCaCert = Ikev2VpnProfile.certificateToPemString(mServerRootCa); + profile.ipsecUserCert = Ikev2VpnProfile.certificateToPemString(mUserCert); + + final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile); + assertNull(result.getUsername()); + assertNull(result.getPassword()); + assertNull(result.getUserCert()); + assertNull(result.getRsaPrivateKey()); + assertNull(result.getServerRootCaCert()); + } + + @Test + public void testUsernamePasswordFromVpnProfileDiscardsIrrelevantValues() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa); + final VpnProfile profile = builder.build().toVpnProfile(); + profile.ipsecSecret = new String(PSK_BYTES); + profile.ipsecUserCert = Ikev2VpnProfile.certificateToPemString(mUserCert); + + final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile); + assertNull(result.getPresharedKey()); + assertNull(result.getUserCert()); + assertNull(result.getRsaPrivateKey()); + } + + @Test + public void testRsaFromVpnProfileDiscardsIrrelevantValues() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa); + final VpnProfile profile = builder.build().toVpnProfile(); + profile.username = USERNAME_STRING; + profile.password = PASSWORD_STRING; + + final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile); + assertNull(result.getUsername()); + assertNull(result.getPassword()); + assertNull(result.getPresharedKey()); + } + + @Test + public void testPskConversionIsLossless() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthPsk(PSK_BYTES); + final Ikev2VpnProfile ikeProfile = builder.build(); + + assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile())); + } + + @Test + public void testUsernamePasswordConversionIsLossless() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa); + final Ikev2VpnProfile ikeProfile = builder.build(); + + assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile())); + } + + @Test + public void testRsaConversionIsLossless() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa); + final Ikev2VpnProfile ikeProfile = builder.build(); + + assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile())); + } + + private static class CertificateAndKey { + public final X509Certificate cert; + public final PrivateKey key; + + CertificateAndKey(X509Certificate cert, PrivateKey key) { + this.cert = cert; + this.key = key; + } + } + + private static CertificateAndKey generateRandomCertAndKeyPair() throws Exception { + final Date validityBeginDate = + new Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1L)); + final Date validityEndDate = + new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1L)); + + // Generate a keypair + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(512); + final KeyPair keyPair = keyPairGenerator.generateKeyPair(); + + final X500Principal dnName = new X500Principal("CN=test.android.com"); + final X509V1CertificateGenerator certGen = new X509V1CertificateGenerator(); + certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); + certGen.setSubjectDN(dnName); + certGen.setIssuerDN(dnName); + certGen.setNotBefore(validityBeginDate); + certGen.setNotAfter(validityEndDate); + certGen.setPublicKey(keyPair.getPublic()); + certGen.setSignatureAlgorithm("SHA256WithRSAEncryption"); + + final X509Certificate cert = certGen.generate(keyPair.getPrivate(), "AndroidOpenSSL"); + return new CertificateAndKey(cert, keyPair.getPrivate()); + } +} diff --git a/tests/unit/java/android/net/IpMemoryStoreTest.java b/tests/unit/java/android/net/IpMemoryStoreTest.java new file mode 100644 index 0000000000..0b13800bc5 --- /dev/null +++ b/tests/unit/java/android/net/IpMemoryStoreTest.java @@ -0,0 +1,332 @@ +/* + * 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.net.ipmemorystore.Blob; +import android.net.ipmemorystore.IOnStatusListener; +import android.net.ipmemorystore.NetworkAttributes; +import android.net.ipmemorystore.NetworkAttributesParcelable; +import android.net.ipmemorystore.Status; +import android.net.networkstack.ModuleNetworkStackClient; +import android.os.RemoteException; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.net.UnknownHostException; +import java.util.Arrays; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class IpMemoryStoreTest { + private static final String TAG = IpMemoryStoreTest.class.getSimpleName(); + private static final String TEST_CLIENT_ID = "testClientId"; + private static final String TEST_DATA_NAME = "testData"; + private static final String TEST_OTHER_DATA_NAME = TEST_DATA_NAME + "Other"; + private static final byte[] TEST_BLOB_DATA = new byte[] { -3, 6, 8, -9, 12, + -128, 0, 89, 112, 91, -34 }; + private static final NetworkAttributes TEST_NETWORK_ATTRIBUTES = buildTestNetworkAttributes( + "hint", 219); + + @Mock + Context mMockContext; + @Mock + ModuleNetworkStackClient mModuleNetworkStackClient; + @Mock + IIpMemoryStore mMockService; + @Mock + IOnStatusListener mIOnStatusListener; + IpMemoryStore mStore; + + @Captor + ArgumentCaptor mCbCaptor; + @Captor + ArgumentCaptor mNapCaptor; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + private void startIpMemoryStore(boolean supplyService) { + if (supplyService) { + doAnswer(invocation -> { + ((IIpMemoryStoreCallbacks) invocation.getArgument(0)) + .onIpMemoryStoreFetched(mMockService); + return null; + }).when(mModuleNetworkStackClient).fetchIpMemoryStore(any()); + } else { + doNothing().when(mModuleNetworkStackClient).fetchIpMemoryStore(mCbCaptor.capture()); + } + mStore = new IpMemoryStore(mMockContext) { + @Override + protected ModuleNetworkStackClient getModuleNetworkStackClient(Context ctx) { + return mModuleNetworkStackClient; + } + }; + } + + private static NetworkAttributes buildTestNetworkAttributes(String hint, int mtu) { + return new NetworkAttributes.Builder() + .setCluster(hint) + .setMtu(mtu) + .build(); + } + + @Test + public void testNetworkAttributes() throws Exception { + startIpMemoryStore(true /* supplyService */); + final String l2Key = "fakeKey"; + + mStore.storeNetworkAttributes(l2Key, TEST_NETWORK_ATTRIBUTES, + status -> assertTrue("Store not successful : " + status.resultCode, + status.isSuccess())); + verify(mMockService, times(1)).storeNetworkAttributes(eq(l2Key), + mNapCaptor.capture(), any()); + assertEquals(TEST_NETWORK_ATTRIBUTES, new NetworkAttributes(mNapCaptor.getValue())); + + mStore.retrieveNetworkAttributes(l2Key, + (status, key, attr) -> { + assertTrue("Retrieve network attributes not successful : " + + status.resultCode, status.isSuccess()); + assertEquals(l2Key, key); + assertEquals(TEST_NETWORK_ATTRIBUTES, attr); + }); + + verify(mMockService, times(1)).retrieveNetworkAttributes(eq(l2Key), any()); + } + + @Test + public void testPrivateData() throws RemoteException { + startIpMemoryStore(true /* supplyService */); + final Blob b = new Blob(); + b.data = TEST_BLOB_DATA; + final String l2Key = "fakeKey"; + + mStore.storeBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME, b, + status -> { + assertTrue("Store not successful : " + status.resultCode, status.isSuccess()); + }); + verify(mMockService, times(1)).storeBlob(eq(l2Key), eq(TEST_CLIENT_ID), eq(TEST_DATA_NAME), + eq(b), any()); + + mStore.retrieveBlob(l2Key, TEST_CLIENT_ID, TEST_OTHER_DATA_NAME, + (status, key, name, data) -> { + assertTrue("Retrieve blob status not successful : " + status.resultCode, + status.isSuccess()); + assertEquals(l2Key, key); + assertEquals(name, TEST_DATA_NAME); + assertTrue(Arrays.equals(b.data, data.data)); + }); + verify(mMockService, times(1)).retrieveBlob(eq(l2Key), eq(TEST_CLIENT_ID), + eq(TEST_OTHER_DATA_NAME), any()); + } + + @Test + public void testFindL2Key() + throws UnknownHostException, RemoteException, Exception { + startIpMemoryStore(true /* supplyService */); + final String l2Key = "fakeKey"; + + mStore.findL2Key(TEST_NETWORK_ATTRIBUTES, + (status, key) -> { + assertTrue("Retrieve network sameness not successful : " + status.resultCode, + status.isSuccess()); + assertEquals(l2Key, key); + }); + verify(mMockService, times(1)).findL2Key(mNapCaptor.capture(), any()); + assertEquals(TEST_NETWORK_ATTRIBUTES, new NetworkAttributes(mNapCaptor.getValue())); + } + + @Test + public void testIsSameNetwork() throws UnknownHostException, RemoteException { + startIpMemoryStore(true /* supplyService */); + final String l2Key1 = "fakeKey1"; + final String l2Key2 = "fakeKey2"; + + mStore.isSameNetwork(l2Key1, l2Key2, + (status, answer) -> { + assertFalse("Retrieve network sameness suspiciously successful : " + + status.resultCode, status.isSuccess()); + assertEquals(Status.ERROR_ILLEGAL_ARGUMENT, status.resultCode); + assertNull(answer); + }); + verify(mMockService, times(1)).isSameNetwork(eq(l2Key1), eq(l2Key2), any()); + } + + @Test + public void testEnqueuedIpMsRequests() throws Exception { + startIpMemoryStore(false /* supplyService */); + + final Blob b = new Blob(); + b.data = TEST_BLOB_DATA; + final String l2Key = "fakeKey"; + + // enqueue multiple ipms requests + mStore.storeNetworkAttributes(l2Key, TEST_NETWORK_ATTRIBUTES, + status -> assertTrue("Store not successful : " + status.resultCode, + status.isSuccess())); + mStore.retrieveNetworkAttributes(l2Key, + (status, key, attr) -> { + assertTrue("Retrieve network attributes not successful : " + + status.resultCode, status.isSuccess()); + assertEquals(l2Key, key); + assertEquals(TEST_NETWORK_ATTRIBUTES, attr); + }); + mStore.storeBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME, b, + status -> assertTrue("Store not successful : " + status.resultCode, + status.isSuccess())); + mStore.retrieveBlob(l2Key, TEST_CLIENT_ID, TEST_OTHER_DATA_NAME, + (status, key, name, data) -> { + assertTrue("Retrieve blob status not successful : " + status.resultCode, + status.isSuccess()); + assertEquals(l2Key, key); + assertEquals(name, TEST_DATA_NAME); + assertTrue(Arrays.equals(b.data, data.data)); + }); + + // get ipms service ready + mCbCaptor.getValue().onIpMemoryStoreFetched(mMockService); + + InOrder inOrder = inOrder(mMockService); + + inOrder.verify(mMockService).storeNetworkAttributes(eq(l2Key), mNapCaptor.capture(), any()); + inOrder.verify(mMockService).retrieveNetworkAttributes(eq(l2Key), any()); + inOrder.verify(mMockService).storeBlob(eq(l2Key), eq(TEST_CLIENT_ID), eq(TEST_DATA_NAME), + eq(b), any()); + inOrder.verify(mMockService).retrieveBlob(eq(l2Key), eq(TEST_CLIENT_ID), + eq(TEST_OTHER_DATA_NAME), any()); + assertEquals(TEST_NETWORK_ATTRIBUTES, new NetworkAttributes(mNapCaptor.getValue())); + } + + @Test + public void testEnqueuedIpMsRequestsWithException() throws Exception { + startIpMemoryStore(true /* supplyService */); + doThrow(RemoteException.class).when(mMockService).retrieveNetworkAttributes(any(), any()); + + final Blob b = new Blob(); + b.data = TEST_BLOB_DATA; + final String l2Key = "fakeKey"; + + // enqueue multiple ipms requests + mStore.storeNetworkAttributes(l2Key, TEST_NETWORK_ATTRIBUTES, + status -> assertTrue("Store not successful : " + status.resultCode, + status.isSuccess())); + mStore.retrieveNetworkAttributes(l2Key, + (status, key, attr) -> { + assertTrue("Retrieve network attributes not successful : " + + status.resultCode, status.isSuccess()); + assertEquals(l2Key, key); + assertEquals(TEST_NETWORK_ATTRIBUTES, attr); + }); + mStore.storeBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME, b, + status -> assertTrue("Store not successful : " + status.resultCode, + status.isSuccess())); + mStore.retrieveBlob(l2Key, TEST_CLIENT_ID, TEST_OTHER_DATA_NAME, + (status, key, name, data) -> { + assertTrue("Retrieve blob status not successful : " + status.resultCode, + status.isSuccess()); + assertEquals(l2Key, key); + assertEquals(name, TEST_DATA_NAME); + assertTrue(Arrays.equals(b.data, data.data)); + }); + + // verify the rest of the queue is still processed in order even if the remote exception + // occurs when calling one or more requests + InOrder inOrder = inOrder(mMockService); + + inOrder.verify(mMockService).storeNetworkAttributes(eq(l2Key), mNapCaptor.capture(), any()); + inOrder.verify(mMockService).storeBlob(eq(l2Key), eq(TEST_CLIENT_ID), eq(TEST_DATA_NAME), + eq(b), any()); + inOrder.verify(mMockService).retrieveBlob(eq(l2Key), eq(TEST_CLIENT_ID), + eq(TEST_OTHER_DATA_NAME), any()); + assertEquals(TEST_NETWORK_ATTRIBUTES, new NetworkAttributes(mNapCaptor.getValue())); + } + + @Test + public void testEnqueuedIpMsRequestsCallbackFunctionWithException() throws Exception { + startIpMemoryStore(true /* supplyService */); + + final Blob b = new Blob(); + b.data = TEST_BLOB_DATA; + final String l2Key = "fakeKey"; + + // enqueue multiple ipms requests + mStore.storeNetworkAttributes(l2Key, TEST_NETWORK_ATTRIBUTES, + status -> assertTrue("Store not successful : " + status.resultCode, + status.isSuccess())); + mStore.retrieveNetworkAttributes(l2Key, + (status, key, attr) -> { + throw new RuntimeException("retrieveNetworkAttributes test"); + }); + mStore.storeBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME, b, + status -> { + throw new RuntimeException("storeBlob test"); + }); + mStore.retrieveBlob(l2Key, TEST_CLIENT_ID, TEST_OTHER_DATA_NAME, + (status, key, name, data) -> { + assertTrue("Retrieve blob status not successful : " + status.resultCode, + status.isSuccess()); + assertEquals(l2Key, key); + assertEquals(name, TEST_DATA_NAME); + assertTrue(Arrays.equals(b.data, data.data)); + }); + + // verify the rest of the queue is still processed in order even if when one or more + // callback throw the remote exception + InOrder inOrder = inOrder(mMockService); + + inOrder.verify(mMockService).storeNetworkAttributes(eq(l2Key), mNapCaptor.capture(), + any()); + inOrder.verify(mMockService).retrieveNetworkAttributes(eq(l2Key), any()); + inOrder.verify(mMockService).storeBlob(eq(l2Key), eq(TEST_CLIENT_ID), eq(TEST_DATA_NAME), + eq(b), any()); + inOrder.verify(mMockService).retrieveBlob(eq(l2Key), eq(TEST_CLIENT_ID), + eq(TEST_OTHER_DATA_NAME), any()); + assertEquals(TEST_NETWORK_ATTRIBUTES, new NetworkAttributes(mNapCaptor.getValue())); + } + + @Test + public void testFactoryReset() throws RemoteException { + startIpMemoryStore(true /* supplyService */); + mStore.factoryReset(); + verify(mMockService, times(1)).factoryReset(); + } +} diff --git a/tests/unit/java/android/net/IpSecAlgorithmTest.java b/tests/unit/java/android/net/IpSecAlgorithmTest.java new file mode 100644 index 0000000000..3a8d6004f6 --- /dev/null +++ b/tests/unit/java/android/net/IpSecAlgorithmTest.java @@ -0,0 +1,226 @@ +/* + * 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 android.net; + +import static android.net.IpSecAlgorithm.ALGO_TO_REQUIRED_FIRST_SDK; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import android.content.res.Resources; +import android.os.Build; +import android.os.Parcel; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.CollectionUtils; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.AbstractMap.SimpleEntry; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map.Entry; +import java.util.Random; +import java.util.Set; + +/** Unit tests for {@link IpSecAlgorithm}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class IpSecAlgorithmTest { + private static final byte[] KEY_MATERIAL; + + private final Resources mMockResources = mock(Resources.class); + + static { + KEY_MATERIAL = new byte[128]; + new Random().nextBytes(KEY_MATERIAL); + }; + + private static byte[] generateKey(int keyLenInBits) { + return Arrays.copyOf(KEY_MATERIAL, keyLenInBits / 8); + } + + @Test + public void testNoTruncLen() throws Exception { + Entry[] authAndAeadList = + new Entry[] { + new SimpleEntry<>(IpSecAlgorithm.AUTH_HMAC_MD5, 128), + new SimpleEntry<>(IpSecAlgorithm.AUTH_HMAC_SHA1, 160), + new SimpleEntry<>(IpSecAlgorithm.AUTH_HMAC_SHA256, 256), + new SimpleEntry<>(IpSecAlgorithm.AUTH_HMAC_SHA384, 384), + new SimpleEntry<>(IpSecAlgorithm.AUTH_HMAC_SHA512, 512), + new SimpleEntry<>(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, 224), + }; + + // Expect auth and aead algorithms to throw errors if trunclen is omitted. + for (Entry algData : authAndAeadList) { + try { + new IpSecAlgorithm( + algData.getKey(), Arrays.copyOf(KEY_MATERIAL, algData.getValue() / 8)); + fail("Expected exception on unprovided auth trunclen"); + } catch (IllegalArgumentException expected) { + } + } + + // Ensure crypt works with no truncation length supplied. + new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, Arrays.copyOf(KEY_MATERIAL, 256 / 8)); + } + + private void checkAuthKeyAndTruncLenValidation(String algoName, int keyLen, int truncLen) + throws Exception { + new IpSecAlgorithm(algoName, generateKey(keyLen), truncLen); + + try { + new IpSecAlgorithm(algoName, generateKey(keyLen)); + fail("Expected exception on unprovided auth trunclen"); + } catch (IllegalArgumentException pass) { + } + + try { + new IpSecAlgorithm(algoName, generateKey(keyLen + 8), truncLen); + fail("Invalid key length not validated"); + } catch (IllegalArgumentException pass) { + } + + try { + new IpSecAlgorithm(algoName, generateKey(keyLen), truncLen + 1); + fail("Invalid truncation length not validated"); + } catch (IllegalArgumentException pass) { + } + } + + private void checkCryptKeyLenValidation(String algoName, int keyLen) throws Exception { + new IpSecAlgorithm(algoName, generateKey(keyLen)); + + try { + new IpSecAlgorithm(algoName, generateKey(keyLen + 8)); + fail("Invalid key length not validated"); + } catch (IllegalArgumentException pass) { + } + } + + @Test + public void testValidationForAlgosAddedInS() throws Exception { + if (Build.VERSION.FIRST_SDK_INT <= Build.VERSION_CODES.R) { + return; + } + + for (int len : new int[] {160, 224, 288}) { + checkCryptKeyLenValidation(IpSecAlgorithm.CRYPT_AES_CTR, len); + } + checkAuthKeyAndTruncLenValidation(IpSecAlgorithm.AUTH_AES_XCBC, 128, 96); + checkAuthKeyAndTruncLenValidation(IpSecAlgorithm.AUTH_AES_CMAC, 128, 96); + checkAuthKeyAndTruncLenValidation(IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305, 288, 128); + } + + @Test + public void testTruncLenValidation() throws Exception { + for (int truncLen : new int[] {256, 512}) { + new IpSecAlgorithm( + IpSecAlgorithm.AUTH_HMAC_SHA512, + Arrays.copyOf(KEY_MATERIAL, 512 / 8), + truncLen); + } + + for (int truncLen : new int[] {255, 513}) { + try { + new IpSecAlgorithm( + IpSecAlgorithm.AUTH_HMAC_SHA512, + Arrays.copyOf(KEY_MATERIAL, 512 / 8), + truncLen); + fail("Invalid truncation length not validated"); + } catch (IllegalArgumentException pass) { + } + } + } + + @Test + public void testLenValidation() throws Exception { + for (int len : new int[] {128, 192, 256}) { + new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, Arrays.copyOf(KEY_MATERIAL, len / 8)); + } + try { + new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, Arrays.copyOf(KEY_MATERIAL, 384 / 8)); + fail("Invalid key length not validated"); + } catch (IllegalArgumentException pass) { + } + } + + @Test + public void testAlgoNameValidation() throws Exception { + try { + new IpSecAlgorithm("rot13", Arrays.copyOf(KEY_MATERIAL, 128 / 8)); + fail("Invalid algorithm name not validated"); + } catch (IllegalArgumentException pass) { + } + } + + @Test + public void testParcelUnparcel() throws Exception { + IpSecAlgorithm init = + new IpSecAlgorithm( + IpSecAlgorithm.AUTH_HMAC_SHA512, Arrays.copyOf(KEY_MATERIAL, 512 / 8), 256); + + Parcel p = Parcel.obtain(); + p.setDataPosition(0); + init.writeToParcel(p, 0); + + p.setDataPosition(0); + IpSecAlgorithm fin = IpSecAlgorithm.CREATOR.createFromParcel(p); + assertTrue("Parcel/Unparcel failed!", IpSecAlgorithm.equals(init, fin)); + p.recycle(); + } + + private static Set getMandatoryAlgos() { + return CollectionUtils.filter( + ALGO_TO_REQUIRED_FIRST_SDK.keySet(), + i -> Build.VERSION.FIRST_SDK_INT >= ALGO_TO_REQUIRED_FIRST_SDK.get(i)); + } + + private static Set getOptionalAlgos() { + return CollectionUtils.filter( + ALGO_TO_REQUIRED_FIRST_SDK.keySet(), + i -> Build.VERSION.FIRST_SDK_INT < ALGO_TO_REQUIRED_FIRST_SDK.get(i)); + } + + @Test + public void testGetSupportedAlgorithms() throws Exception { + assertTrue(IpSecAlgorithm.getSupportedAlgorithms().containsAll(getMandatoryAlgos())); + assertTrue(ALGO_TO_REQUIRED_FIRST_SDK.keySet().containsAll( + IpSecAlgorithm.getSupportedAlgorithms())); + } + + @Test + public void testLoadAlgos() throws Exception { + final Set optionalAlgoSet = getOptionalAlgos(); + final String[] optionalAlgos = optionalAlgoSet.toArray(new String[0]); + + doReturn(optionalAlgos).when(mMockResources) + .getStringArray(com.android.internal.R.array.config_optionalIpSecAlgorithms); + + final Set enabledAlgos = new HashSet<>(IpSecAlgorithm.loadAlgos(mMockResources)); + final Set expectedAlgos = ALGO_TO_REQUIRED_FIRST_SDK.keySet(); + + assertEquals(expectedAlgos, enabledAlgos); + } +} diff --git a/tests/unit/java/android/net/IpSecConfigTest.java b/tests/unit/java/android/net/IpSecConfigTest.java new file mode 100644 index 0000000000..25e225ef30 --- /dev/null +++ b/tests/unit/java/android/net/IpSecConfigTest.java @@ -0,0 +1,103 @@ +/* + * 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 android.net; + +import static com.android.testutils.ParcelUtils.assertParcelSane; +import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link IpSecConfig}. */ +@SmallTest +@RunWith(JUnit4.class) +public class IpSecConfigTest { + + @Test + public void testDefaults() throws Exception { + IpSecConfig c = new IpSecConfig(); + assertEquals(IpSecTransform.MODE_TRANSPORT, c.getMode()); + assertEquals("", c.getSourceAddress()); + assertEquals("", c.getDestinationAddress()); + assertNull(c.getNetwork()); + assertEquals(IpSecTransform.ENCAP_NONE, c.getEncapType()); + assertEquals(IpSecManager.INVALID_RESOURCE_ID, c.getEncapSocketResourceId()); + assertEquals(0, c.getEncapRemotePort()); + assertEquals(0, c.getNattKeepaliveInterval()); + assertNull(c.getEncryption()); + assertNull(c.getAuthentication()); + assertEquals(IpSecManager.INVALID_RESOURCE_ID, c.getSpiResourceId()); + assertEquals(0, c.getXfrmInterfaceId()); + } + + private IpSecConfig getSampleConfig() { + IpSecConfig c = new IpSecConfig(); + c.setMode(IpSecTransform.MODE_TUNNEL); + c.setSourceAddress("0.0.0.0"); + c.setDestinationAddress("1.2.3.4"); + c.setSpiResourceId(1984); + c.setEncryption( + new IpSecAlgorithm( + IpSecAlgorithm.CRYPT_AES_CBC, + new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF})); + c.setAuthentication( + new IpSecAlgorithm( + IpSecAlgorithm.AUTH_HMAC_MD5, + new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0}, + 128)); + c.setAuthenticatedEncryption( + new IpSecAlgorithm( + IpSecAlgorithm.AUTH_CRYPT_AES_GCM, + new byte[] { + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0, 1, 2, 3, 4 + }, + 128)); + c.setEncapType(android.system.OsConstants.UDP_ENCAP_ESPINUDP); + c.setEncapSocketResourceId(7); + c.setEncapRemotePort(22); + c.setNattKeepaliveInterval(42); + c.setMarkValue(12); + c.setMarkMask(23); + c.setXfrmInterfaceId(34); + + return c; + } + + @Test + public void testCopyConstructor() { + IpSecConfig original = getSampleConfig(); + IpSecConfig copy = new IpSecConfig(original); + + assertEquals(original, copy); + assertNotSame(original, copy); + } + + @Test + public void testParcelUnparcel() { + assertParcelingIsLossless(new IpSecConfig()); + + IpSecConfig c = getSampleConfig(); + assertParcelSane(c, 15); + } +} diff --git a/tests/unit/java/android/net/IpSecManagerTest.java b/tests/unit/java/android/net/IpSecManagerTest.java new file mode 100644 index 0000000000..730e2d56bd --- /dev/null +++ b/tests/unit/java/android/net/IpSecManagerTest.java @@ -0,0 +1,305 @@ +/* + * 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 android.net; + +import static android.system.OsConstants.AF_INET; +import static android.system.OsConstants.IPPROTO_UDP; +import static android.system.OsConstants.SOCK_DGRAM; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.system.Os; +import android.test.mock.MockContext; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.IpSecService; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; + +/** Unit tests for {@link IpSecManager}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class IpSecManagerTest { + + private static final int TEST_UDP_ENCAP_PORT = 34567; + private static final int DROID_SPI = 0xD1201D; + private static final int DUMMY_RESOURCE_ID = 0x1234; + + private static final InetAddress GOOGLE_DNS_4; + private static final String VTI_INTF_NAME = "ipsec_test"; + private static final InetAddress VTI_LOCAL_ADDRESS; + private static final LinkAddress VTI_INNER_ADDRESS = new LinkAddress("10.0.1.1/24"); + + static { + try { + // Google Public DNS Addresses; + GOOGLE_DNS_4 = InetAddress.getByName("8.8.8.8"); + VTI_LOCAL_ADDRESS = InetAddress.getByName("8.8.4.4"); + } catch (UnknownHostException e) { + throw new RuntimeException("Could not resolve DNS Addresses", e); + } + } + + private IpSecService mMockIpSecService; + private IpSecManager mIpSecManager; + private MockContext mMockContext = new MockContext() { + @Override + public String getOpPackageName() { + return "fooPackage"; + } + }; + + @Before + public void setUp() throws Exception { + mMockIpSecService = mock(IpSecService.class); + mIpSecManager = new IpSecManager(mMockContext, mMockIpSecService); + } + + /* + * Allocate a specific SPI + * Close SPIs + */ + @Test + public void testAllocSpi() throws Exception { + IpSecSpiResponse spiResp = + new IpSecSpiResponse(IpSecManager.Status.OK, DUMMY_RESOURCE_ID, DROID_SPI); + when(mMockIpSecService.allocateSecurityParameterIndex( + eq(GOOGLE_DNS_4.getHostAddress()), + eq(DROID_SPI), + anyObject())) + .thenReturn(spiResp); + + IpSecManager.SecurityParameterIndex droidSpi = + mIpSecManager.allocateSecurityParameterIndex(GOOGLE_DNS_4, DROID_SPI); + assertEquals(DROID_SPI, droidSpi.getSpi()); + + droidSpi.close(); + + verify(mMockIpSecService).releaseSecurityParameterIndex(DUMMY_RESOURCE_ID); + } + + @Test + public void testAllocRandomSpi() throws Exception { + IpSecSpiResponse spiResp = + new IpSecSpiResponse(IpSecManager.Status.OK, DUMMY_RESOURCE_ID, DROID_SPI); + when(mMockIpSecService.allocateSecurityParameterIndex( + eq(GOOGLE_DNS_4.getHostAddress()), + eq(IpSecManager.INVALID_SECURITY_PARAMETER_INDEX), + anyObject())) + .thenReturn(spiResp); + + IpSecManager.SecurityParameterIndex randomSpi = + mIpSecManager.allocateSecurityParameterIndex(GOOGLE_DNS_4); + + assertEquals(DROID_SPI, randomSpi.getSpi()); + + randomSpi.close(); + + verify(mMockIpSecService).releaseSecurityParameterIndex(DUMMY_RESOURCE_ID); + } + + /* + * Throws resource unavailable exception + */ + @Test + public void testAllocSpiResUnavailableException() throws Exception { + IpSecSpiResponse spiResp = + new IpSecSpiResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE, 0, 0); + when(mMockIpSecService.allocateSecurityParameterIndex( + anyString(), anyInt(), anyObject())) + .thenReturn(spiResp); + + try { + mIpSecManager.allocateSecurityParameterIndex(GOOGLE_DNS_4); + fail("ResourceUnavailableException was not thrown"); + } catch (IpSecManager.ResourceUnavailableException e) { + } + } + + /* + * Throws spi unavailable exception + */ + @Test + public void testAllocSpiSpiUnavailableException() throws Exception { + IpSecSpiResponse spiResp = new IpSecSpiResponse(IpSecManager.Status.SPI_UNAVAILABLE, 0, 0); + when(mMockIpSecService.allocateSecurityParameterIndex( + anyString(), anyInt(), anyObject())) + .thenReturn(spiResp); + + try { + mIpSecManager.allocateSecurityParameterIndex(GOOGLE_DNS_4); + fail("ResourceUnavailableException was not thrown"); + } catch (IpSecManager.ResourceUnavailableException e) { + } + } + + /* + * Should throw exception when request spi 0 in IpSecManager + */ + @Test + public void testRequestAllocInvalidSpi() throws Exception { + try { + mIpSecManager.allocateSecurityParameterIndex(GOOGLE_DNS_4, 0); + fail("Able to allocate invalid spi"); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void testOpenEncapsulationSocket() throws Exception { + IpSecUdpEncapResponse udpEncapResp = + new IpSecUdpEncapResponse( + IpSecManager.Status.OK, + DUMMY_RESOURCE_ID, + TEST_UDP_ENCAP_PORT, + Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)); + when(mMockIpSecService.openUdpEncapsulationSocket(eq(TEST_UDP_ENCAP_PORT), anyObject())) + .thenReturn(udpEncapResp); + + IpSecManager.UdpEncapsulationSocket encapSocket = + mIpSecManager.openUdpEncapsulationSocket(TEST_UDP_ENCAP_PORT); + assertNotNull(encapSocket.getFileDescriptor()); + assertEquals(TEST_UDP_ENCAP_PORT, encapSocket.getPort()); + + encapSocket.close(); + + verify(mMockIpSecService).closeUdpEncapsulationSocket(DUMMY_RESOURCE_ID); + } + + @Test + public void testApplyTransportModeTransformEnsuresSocketCreation() throws Exception { + Socket socket = new Socket(); + IpSecConfig dummyConfig = new IpSecConfig(); + IpSecTransform dummyTransform = new IpSecTransform(null, dummyConfig); + + // Even if underlying SocketImpl is not initalized, this should force the init, and + // thereby succeed. + mIpSecManager.applyTransportModeTransform( + socket, IpSecManager.DIRECTION_IN, dummyTransform); + + // Check to make sure the FileDescriptor is non-null + assertNotNull(socket.getFileDescriptor$()); + } + + @Test + public void testRemoveTransportModeTransformsForcesSocketCreation() throws Exception { + Socket socket = new Socket(); + + // Even if underlying SocketImpl is not initalized, this should force the init, and + // thereby succeed. + mIpSecManager.removeTransportModeTransforms(socket); + + // Check to make sure the FileDescriptor is non-null + assertNotNull(socket.getFileDescriptor$()); + } + + @Test + public void testOpenEncapsulationSocketOnRandomPort() throws Exception { + IpSecUdpEncapResponse udpEncapResp = + new IpSecUdpEncapResponse( + IpSecManager.Status.OK, + DUMMY_RESOURCE_ID, + TEST_UDP_ENCAP_PORT, + Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)); + + when(mMockIpSecService.openUdpEncapsulationSocket(eq(0), anyObject())) + .thenReturn(udpEncapResp); + + IpSecManager.UdpEncapsulationSocket encapSocket = + mIpSecManager.openUdpEncapsulationSocket(); + + assertNotNull(encapSocket.getFileDescriptor()); + assertEquals(TEST_UDP_ENCAP_PORT, encapSocket.getPort()); + + encapSocket.close(); + + verify(mMockIpSecService).closeUdpEncapsulationSocket(DUMMY_RESOURCE_ID); + } + + @Test + public void testOpenEncapsulationSocketWithInvalidPort() throws Exception { + try { + mIpSecManager.openUdpEncapsulationSocket(IpSecManager.INVALID_SECURITY_PARAMETER_INDEX); + fail("IllegalArgumentException was not thrown"); + } catch (IllegalArgumentException e) { + } + } + + // TODO: add test when applicable transform builder interface is available + + private IpSecManager.IpSecTunnelInterface createAndValidateVti(int resourceId, String intfName) + throws Exception { + IpSecTunnelInterfaceResponse dummyResponse = + new IpSecTunnelInterfaceResponse(IpSecManager.Status.OK, resourceId, intfName); + when(mMockIpSecService.createTunnelInterface( + eq(VTI_LOCAL_ADDRESS.getHostAddress()), eq(GOOGLE_DNS_4.getHostAddress()), + anyObject(), anyObject(), anyString())) + .thenReturn(dummyResponse); + + IpSecManager.IpSecTunnelInterface tunnelIntf = mIpSecManager.createIpSecTunnelInterface( + VTI_LOCAL_ADDRESS, GOOGLE_DNS_4, mock(Network.class)); + + assertNotNull(tunnelIntf); + return tunnelIntf; + } + + @Test + public void testCreateVti() throws Exception { + IpSecManager.IpSecTunnelInterface tunnelIntf = + createAndValidateVti(DUMMY_RESOURCE_ID, VTI_INTF_NAME); + + assertEquals(VTI_INTF_NAME, tunnelIntf.getInterfaceName()); + + tunnelIntf.close(); + verify(mMockIpSecService).deleteTunnelInterface(eq(DUMMY_RESOURCE_ID), anyString()); + } + + @Test + public void testAddRemoveAddressesFromVti() throws Exception { + IpSecManager.IpSecTunnelInterface tunnelIntf = + createAndValidateVti(DUMMY_RESOURCE_ID, VTI_INTF_NAME); + + tunnelIntf.addAddress(VTI_INNER_ADDRESS.getAddress(), + VTI_INNER_ADDRESS.getPrefixLength()); + verify(mMockIpSecService) + .addAddressToTunnelInterface( + eq(DUMMY_RESOURCE_ID), eq(VTI_INNER_ADDRESS), anyString()); + + tunnelIntf.removeAddress(VTI_INNER_ADDRESS.getAddress(), + VTI_INNER_ADDRESS.getPrefixLength()); + verify(mMockIpSecService) + .addAddressToTunnelInterface( + eq(DUMMY_RESOURCE_ID), eq(VTI_INNER_ADDRESS), anyString()); + } +} diff --git a/tests/unit/java/android/net/IpSecTransformTest.java b/tests/unit/java/android/net/IpSecTransformTest.java new file mode 100644 index 0000000000..424f23dbba --- /dev/null +++ b/tests/unit/java/android/net/IpSecTransformTest.java @@ -0,0 +1,62 @@ +/* + * 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 android.net; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link IpSecTransform}. */ +@SmallTest +@RunWith(JUnit4.class) +public class IpSecTransformTest { + + @Test + public void testCreateTransformCopiesConfig() { + // Create a config with a few parameters to make sure it's not empty + IpSecConfig config = new IpSecConfig(); + config.setSourceAddress("0.0.0.0"); + config.setDestinationAddress("1.2.3.4"); + config.setSpiResourceId(1984); + + IpSecTransform preModification = new IpSecTransform(null, config); + + config.setSpiResourceId(1985); + IpSecTransform postModification = new IpSecTransform(null, config); + + assertNotEquals(preModification, postModification); + } + + @Test + public void testCreateTransformsWithSameConfigEqual() { + // Create a config with a few parameters to make sure it's not empty + IpSecConfig config = new IpSecConfig(); + config.setSourceAddress("0.0.0.0"); + config.setDestinationAddress("1.2.3.4"); + config.setSpiResourceId(1984); + + IpSecTransform config1 = new IpSecTransform(null, config); + IpSecTransform config2 = new IpSecTransform(null, config); + + assertEquals(config1, config2); + } +} diff --git a/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java b/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java new file mode 100644 index 0000000000..fc739fbfac --- /dev/null +++ b/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2020 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; + +import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.net.util.KeepalivePacketDataUtil; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.net.InetAddress; +import java.nio.ByteBuffer; + +@RunWith(JUnit4.class) +public final class KeepalivePacketDataUtilTest { + private static final byte[] IPV4_KEEPALIVE_SRC_ADDR = {10, 0, 0, 1}; + private static final byte[] IPV4_KEEPALIVE_DST_ADDR = {10, 0, 0, 5}; + + @Before + public void setUp() {} + + @Test + public void testFromTcpKeepaliveStableParcelable() throws Exception { + final int srcPort = 1234; + final int dstPort = 4321; + final int seq = 0x11111111; + final int ack = 0x22222222; + final int wnd = 8000; + final int wndScale = 2; + final int tos = 4; + final int ttl = 64; + TcpKeepalivePacketData resultData = null; + final TcpKeepalivePacketDataParcelable testInfo = new TcpKeepalivePacketDataParcelable(); + testInfo.srcAddress = IPV4_KEEPALIVE_SRC_ADDR; + testInfo.srcPort = srcPort; + testInfo.dstAddress = IPV4_KEEPALIVE_DST_ADDR; + testInfo.dstPort = dstPort; + testInfo.seq = seq; + testInfo.ack = ack; + testInfo.rcvWnd = wnd; + testInfo.rcvWndScale = wndScale; + testInfo.tos = tos; + testInfo.ttl = ttl; + try { + resultData = KeepalivePacketDataUtil.fromStableParcelable(testInfo); + } catch (InvalidPacketException e) { + fail("InvalidPacketException: " + e); + } + + assertEquals(InetAddress.getByAddress(testInfo.srcAddress), resultData.getSrcAddress()); + assertEquals(InetAddress.getByAddress(testInfo.dstAddress), resultData.getDstAddress()); + assertEquals(testInfo.srcPort, resultData.getSrcPort()); + assertEquals(testInfo.dstPort, resultData.getDstPort()); + assertEquals(testInfo.seq, resultData.tcpSeq); + assertEquals(testInfo.ack, resultData.tcpAck); + assertEquals(testInfo.rcvWnd, resultData.tcpWindow); + assertEquals(testInfo.rcvWndScale, resultData.tcpWindowScale); + assertEquals(testInfo.tos, resultData.ipTos); + assertEquals(testInfo.ttl, resultData.ipTtl); + + assertParcelingIsLossless(resultData); + + final byte[] packet = resultData.getPacket(); + // IP version and IHL + assertEquals(packet[0], 0x45); + // TOS + assertEquals(packet[1], tos); + // TTL + assertEquals(packet[8], ttl); + // Source IP address. + byte[] ip = new byte[4]; + ByteBuffer buf = ByteBuffer.wrap(packet, 12, 4); + buf.get(ip); + assertArrayEquals(ip, IPV4_KEEPALIVE_SRC_ADDR); + // Destination IP address. + buf = ByteBuffer.wrap(packet, 16, 4); + buf.get(ip); + assertArrayEquals(ip, IPV4_KEEPALIVE_DST_ADDR); + + buf = ByteBuffer.wrap(packet, 20, 12); + // Source port. + assertEquals(buf.getShort(), srcPort); + // Destination port. + assertEquals(buf.getShort(), dstPort); + // Sequence number. + assertEquals(buf.getInt(), seq); + // Ack. + assertEquals(buf.getInt(), ack); + // Window size. + buf = ByteBuffer.wrap(packet, 34, 2); + assertEquals(buf.getShort(), wnd >> wndScale); + } + + //TODO: add ipv6 test when ipv6 supported + + @Test + public void testToTcpKeepaliveStableParcelable() throws Exception { + final int srcPort = 1234; + final int dstPort = 4321; + final int sequence = 0x11111111; + final int ack = 0x22222222; + final int wnd = 48_000; + final int wndScale = 2; + final int tos = 4; + final int ttl = 64; + final TcpKeepalivePacketDataParcelable testInfo = new TcpKeepalivePacketDataParcelable(); + testInfo.srcAddress = IPV4_KEEPALIVE_SRC_ADDR; + testInfo.srcPort = srcPort; + testInfo.dstAddress = IPV4_KEEPALIVE_DST_ADDR; + testInfo.dstPort = dstPort; + testInfo.seq = sequence; + testInfo.ack = ack; + testInfo.rcvWnd = wnd; + testInfo.rcvWndScale = wndScale; + testInfo.tos = tos; + testInfo.ttl = ttl; + TcpKeepalivePacketData testData = null; + TcpKeepalivePacketDataParcelable resultData = null; + testData = KeepalivePacketDataUtil.fromStableParcelable(testInfo); + resultData = KeepalivePacketDataUtil.toStableParcelable(testData); + assertArrayEquals(resultData.srcAddress, IPV4_KEEPALIVE_SRC_ADDR); + assertArrayEquals(resultData.dstAddress, IPV4_KEEPALIVE_DST_ADDR); + assertEquals(resultData.srcPort, srcPort); + assertEquals(resultData.dstPort, dstPort); + assertEquals(resultData.seq, sequence); + assertEquals(resultData.ack, ack); + assertEquals(resultData.rcvWnd, wnd); + assertEquals(resultData.rcvWndScale, wndScale); + assertEquals(resultData.tos, tos); + assertEquals(resultData.ttl, ttl); + + final String expected = "" + + "android.net.TcpKeepalivePacketDataParcelable{srcAddress: [10, 0, 0, 1]," + + " srcPort: 1234, dstAddress: [10, 0, 0, 5], dstPort: 4321, seq: 286331153," + + " ack: 572662306, rcvWnd: 48000, rcvWndScale: 2, tos: 4, ttl: 64}"; + assertEquals(expected, resultData.toString()); + } + + @Test + public void testParseTcpKeepalivePacketData() throws Exception { + final int srcPort = 1234; + final int dstPort = 4321; + final int sequence = 0x11111111; + final int ack = 0x22222222; + final int wnd = 4800; + final int wndScale = 2; + final int tos = 4; + final int ttl = 64; + final TcpKeepalivePacketDataParcelable testParcel = new TcpKeepalivePacketDataParcelable(); + testParcel.srcAddress = IPV4_KEEPALIVE_SRC_ADDR; + testParcel.srcPort = srcPort; + testParcel.dstAddress = IPV4_KEEPALIVE_DST_ADDR; + testParcel.dstPort = dstPort; + testParcel.seq = sequence; + testParcel.ack = ack; + testParcel.rcvWnd = wnd; + testParcel.rcvWndScale = wndScale; + testParcel.tos = tos; + testParcel.ttl = ttl; + + final KeepalivePacketData testData = + KeepalivePacketDataUtil.fromStableParcelable(testParcel); + final TcpKeepalivePacketDataParcelable parsedParcelable = + KeepalivePacketDataUtil.parseTcpKeepalivePacketData(testData); + final TcpKeepalivePacketData roundTripData = + KeepalivePacketDataUtil.fromStableParcelable(parsedParcelable); + + // Generated packet is the same, but rcvWnd / wndScale will differ if scale is non-zero + assertTrue(testData.getPacket().length > 0); + assertArrayEquals(testData.getPacket(), roundTripData.getPacket()); + + testParcel.rcvWndScale = 0; + final KeepalivePacketData noScaleTestData = + KeepalivePacketDataUtil.fromStableParcelable(testParcel); + final TcpKeepalivePacketDataParcelable noScaleParsedParcelable = + KeepalivePacketDataUtil.parseTcpKeepalivePacketData(noScaleTestData); + final TcpKeepalivePacketData noScaleRoundTripData = + KeepalivePacketDataUtil.fromStableParcelable(noScaleParsedParcelable); + assertEquals(noScaleTestData, noScaleRoundTripData); + assertTrue(noScaleTestData.getPacket().length > 0); + assertArrayEquals(noScaleTestData.getPacket(), noScaleRoundTripData.getPacket()); + } +} diff --git a/tests/unit/java/android/net/MacAddressTest.java b/tests/unit/java/android/net/MacAddressTest.java new file mode 100644 index 0000000000..6de31f6b4b --- /dev/null +++ b/tests/unit/java/android/net/MacAddressTest.java @@ -0,0 +1,312 @@ +/* + * Copyright 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 android.net; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.net.module.util.MacAddressUtils; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.Inet6Address; +import java.util.Arrays; +import java.util.Random; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class MacAddressTest { + + static class AddrTypeTestCase { + byte[] addr; + int expectedType; + + static AddrTypeTestCase of(int expectedType, int... addr) { + AddrTypeTestCase t = new AddrTypeTestCase(); + t.expectedType = expectedType; + t.addr = toByteArray(addr); + return t; + } + } + + @Test + public void testMacAddrTypes() { + AddrTypeTestCase[] testcases = { + AddrTypeTestCase.of(MacAddress.TYPE_UNKNOWN), + AddrTypeTestCase.of(MacAddress.TYPE_UNKNOWN, 0), + AddrTypeTestCase.of(MacAddress.TYPE_UNKNOWN, 1, 2, 3, 4, 5), + AddrTypeTestCase.of(MacAddress.TYPE_UNKNOWN, 1, 2, 3, 4, 5, 6, 7), + AddrTypeTestCase.of(MacAddress.TYPE_UNICAST, 0xa0, 0xb0, 0xc0, 0xd0, 0xe0, 0xf0), + AddrTypeTestCase.of(MacAddress.TYPE_BROADCAST, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff), + AddrTypeTestCase.of(MacAddress.TYPE_MULTICAST, 1, 2, 3, 4, 5, 6), + AddrTypeTestCase.of(MacAddress.TYPE_MULTICAST, 11, 22, 33, 44, 55, 66), + AddrTypeTestCase.of(MacAddress.TYPE_MULTICAST, 33, 33, 0xaa, 0xbb, 0xcc, 0xdd) + }; + + for (AddrTypeTestCase t : testcases) { + int got = MacAddress.macAddressType(t.addr); + String msg = String.format("expected type of %s to be %s, but got %s", + Arrays.toString(t.addr), t.expectedType, got); + assertEquals(msg, t.expectedType, got); + + if (got != MacAddress.TYPE_UNKNOWN) { + assertEquals(got, MacAddress.fromBytes(t.addr).getAddressType()); + } + } + } + + @Test + public void testToOuiString() { + String[][] macs = { + {"07:00:d3:56:8a:c4", "07:00:d3"}, + {"33:33:aa:bb:cc:dd", "33:33:aa"}, + {"06:00:00:00:00:00", "06:00:00"}, + {"07:00:d3:56:8a:c4", "07:00:d3"} + }; + + for (String[] pair : macs) { + String mac = pair[0]; + String expected = pair[1]; + assertEquals(expected, MacAddress.fromString(mac).toOuiString()); + } + } + + @Test + public void testHexPaddingWhenPrinting() { + String[] macs = { + "07:00:d3:56:8a:c4", + "33:33:aa:bb:cc:dd", + "06:00:00:00:00:00", + "07:00:d3:56:8a:c4" + }; + + for (String mac : macs) { + assertEquals(mac, MacAddress.fromString(mac).toString()); + assertEquals(mac, + MacAddress.stringAddrFromByteAddr(MacAddress.byteAddrFromStringAddr(mac))); + } + } + + @Test + public void testIsMulticastAddress() { + MacAddress[] multicastAddresses = { + MacAddress.BROADCAST_ADDRESS, + MacAddress.fromString("07:00:d3:56:8a:c4"), + MacAddress.fromString("33:33:aa:bb:cc:dd"), + }; + MacAddress[] unicastAddresses = { + MacAddress.ALL_ZEROS_ADDRESS, + MacAddress.fromString("00:01:44:55:66:77"), + MacAddress.fromString("08:00:22:33:44:55"), + MacAddress.fromString("06:00:00:00:00:00"), + }; + + for (MacAddress mac : multicastAddresses) { + String msg = mac.toString() + " expected to be a multicast address"; + assertTrue(msg, MacAddressUtils.isMulticastAddress(mac)); + } + for (MacAddress mac : unicastAddresses) { + String msg = mac.toString() + " expected not to be a multicast address"; + assertFalse(msg, MacAddressUtils.isMulticastAddress(mac)); + } + } + + @Test + public void testIsLocallyAssignedAddress() { + MacAddress[] localAddresses = { + MacAddress.fromString("06:00:00:00:00:00"), + MacAddress.fromString("07:00:d3:56:8a:c4"), + MacAddress.fromString("33:33:aa:bb:cc:dd"), + }; + MacAddress[] universalAddresses = { + MacAddress.fromString("00:01:44:55:66:77"), + MacAddress.fromString("08:00:22:33:44:55"), + }; + + for (MacAddress mac : localAddresses) { + String msg = mac.toString() + " expected to be a locally assigned address"; + assertTrue(msg, mac.isLocallyAssigned()); + } + for (MacAddress mac : universalAddresses) { + String msg = mac.toString() + " expected not to be globally unique address"; + assertFalse(msg, mac.isLocallyAssigned()); + } + } + + @Test + public void testMacAddressConversions() { + final int iterations = 10000; + for (int i = 0; i < iterations; i++) { + MacAddress mac = MacAddressUtils.createRandomUnicastAddress(); + + String stringRepr = mac.toString(); + byte[] bytesRepr = mac.toByteArray(); + + assertEquals(mac, MacAddress.fromString(stringRepr)); + assertEquals(mac, MacAddress.fromBytes(bytesRepr)); + + assertEquals(mac, MacAddress.fromString(MacAddress.stringAddrFromByteAddr(bytesRepr))); + assertEquals(mac, MacAddress.fromBytes(MacAddress.byteAddrFromStringAddr(stringRepr))); + } + } + + @Test + public void testMacAddressRandomGeneration() { + final int iterations = 1000; + final String expectedAndroidOui = "da:a1:19"; + for (int i = 0; i < iterations; i++) { + MacAddress mac = MacAddress.createRandomUnicastAddressWithGoogleBase(); + String stringRepr = mac.toString(); + + assertTrue(stringRepr + " expected to be a locally assigned address", + mac.isLocallyAssigned()); + assertTrue(stringRepr + " expected to begin with " + expectedAndroidOui, + stringRepr.startsWith(expectedAndroidOui)); + } + + final Random r = new Random(); + final String anotherOui = "24:5f:78"; + final String expectedLocalOui = "26:5f:78"; + final MacAddress base = MacAddress.fromString(anotherOui + ":0:0:0"); + for (int i = 0; i < iterations; i++) { + MacAddress mac = MacAddressUtils.createRandomUnicastAddress(base, r); + String stringRepr = mac.toString(); + + assertTrue(stringRepr + " expected to be a locally assigned address", + mac.isLocallyAssigned()); + assertEquals(MacAddress.TYPE_UNICAST, mac.getAddressType()); + assertTrue(stringRepr + " expected to begin with " + expectedLocalOui, + stringRepr.startsWith(expectedLocalOui)); + } + + for (int i = 0; i < iterations; i++) { + MacAddress mac = MacAddressUtils.createRandomUnicastAddress(); + String stringRepr = mac.toString(); + + assertTrue(stringRepr + " expected to be a locally assigned address", + mac.isLocallyAssigned()); + assertEquals(MacAddress.TYPE_UNICAST, mac.getAddressType()); + } + } + + @Test + public void testConstructorInputValidation() { + String[] invalidStringAddresses = { + "", + "abcd", + "1:2:3:4:5", + "1:2:3:4:5:6:7", + "10000:2:3:4:5:6", + }; + + for (String s : invalidStringAddresses) { + try { + MacAddress mac = MacAddress.fromString(s); + fail("MacAddress.fromString(" + s + ") should have failed, but returned " + mac); + } catch (IllegalArgumentException excepted) { + } + } + + try { + MacAddress mac = MacAddress.fromString(null); + fail("MacAddress.fromString(null) should have failed, but returned " + mac); + } catch (NullPointerException excepted) { + } + + byte[][] invalidBytesAddresses = { + {}, + {1,2,3,4,5}, + {1,2,3,4,5,6,7}, + }; + + for (byte[] b : invalidBytesAddresses) { + try { + MacAddress mac = MacAddress.fromBytes(b); + fail("MacAddress.fromBytes(" + Arrays.toString(b) + + ") should have failed, but returned " + mac); + } catch (IllegalArgumentException excepted) { + } + } + + try { + MacAddress mac = MacAddress.fromBytes(null); + fail("MacAddress.fromBytes(null) should have failed, but returned " + mac); + } catch (NullPointerException excepted) { + } + } + + @Test + public void testMatches() { + // match 4 bytes prefix + assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches( + MacAddress.fromString("aa:bb:cc:dd:00:00"), + MacAddress.fromString("ff:ff:ff:ff:00:00"))); + + // match bytes 0,1,2 and 5 + assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches( + MacAddress.fromString("aa:bb:cc:00:00:11"), + MacAddress.fromString("ff:ff:ff:00:00:ff"))); + + // match 34 bit prefix + assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches( + MacAddress.fromString("aa:bb:cc:dd:c0:00"), + MacAddress.fromString("ff:ff:ff:ff:c0:00"))); + + // fail to match 36 bit prefix + assertFalse(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches( + MacAddress.fromString("aa:bb:cc:dd:40:00"), + MacAddress.fromString("ff:ff:ff:ff:f0:00"))); + + // match all 6 bytes + assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches( + MacAddress.fromString("aa:bb:cc:dd:ee:11"), + MacAddress.fromString("ff:ff:ff:ff:ff:ff"))); + + // match none of 6 bytes + assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches( + MacAddress.fromString("00:00:00:00:00:00"), + MacAddress.fromString("00:00:00:00:00:00"))); + } + + /** + * Tests that link-local address generation from MAC is valid. + */ + @Test + public void testLinkLocalFromMacGeneration() { + MacAddress mac = MacAddress.fromString("52:74:f2:b1:a8:7f"); + byte[] inet6ll = {(byte) 0xfe, (byte) 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x74, + (byte) 0xf2, (byte) 0xff, (byte) 0xfe, (byte) 0xb1, (byte) 0xa8, 0x7f}; + Inet6Address llv6 = mac.getLinkLocalIpv6FromEui48Mac(); + assertTrue(llv6.isLinkLocalAddress()); + assertArrayEquals(inet6ll, llv6.getAddress()); + } + + static byte[] toByteArray(int... in) { + byte[] out = new byte[in.length]; + for (int i = 0; i < in.length; i++) { + out[i] = (byte) in[i]; + } + return out; + } +} diff --git a/tests/unit/java/android/net/NetworkIdentityTest.kt b/tests/unit/java/android/net/NetworkIdentityTest.kt new file mode 100644 index 0000000000..eb2b85c145 --- /dev/null +++ b/tests/unit/java/android/net/NetworkIdentityTest.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2021 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 + +import android.net.NetworkIdentity.OEM_NONE +import android.net.NetworkIdentity.OEM_PAID +import android.net.NetworkIdentity.OEM_PRIVATE +import android.net.NetworkIdentity.getOemBitfield +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import kotlin.test.assertEquals + +@RunWith(JUnit4::class) +class NetworkIdentityTest { + @Test + fun testGetOemBitfield() { + val oemNone = NetworkCapabilities().apply { + setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID, false) + setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE, false) + } + val oemPaid = NetworkCapabilities().apply { + setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID, true) + setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE, false) + } + val oemPrivate = NetworkCapabilities().apply { + setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID, false) + setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE, true) + } + val oemAll = NetworkCapabilities().apply { + setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID, true) + setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE, true) + } + + assertEquals(getOemBitfield(oemNone), OEM_NONE) + assertEquals(getOemBitfield(oemPaid), OEM_PAID) + assertEquals(getOemBitfield(oemPrivate), OEM_PRIVATE) + assertEquals(getOemBitfield(oemAll), OEM_PAID or OEM_PRIVATE) + } +} diff --git a/tests/unit/java/android/net/NetworkStatsHistoryTest.java b/tests/unit/java/android/net/NetworkStatsHistoryTest.java new file mode 100644 index 0000000000..13558cd51c --- /dev/null +++ b/tests/unit/java/android/net/NetworkStatsHistoryTest.java @@ -0,0 +1,599 @@ +/* + * Copyright (C) 2011 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; + +import static android.net.NetworkStatsHistory.DataStreamUtils.readVarLong; +import static android.net.NetworkStatsHistory.DataStreamUtils.writeVarLong; +import static android.net.NetworkStatsHistory.Entry.UNKNOWN; +import static android.net.NetworkStatsHistory.FIELD_ALL; +import static android.net.NetworkStatsHistory.FIELD_OPERATIONS; +import static android.net.NetworkStatsHistory.FIELD_RX_BYTES; +import static android.net.NetworkStatsHistory.FIELD_RX_PACKETS; +import static android.net.NetworkStatsHistory.FIELD_TX_BYTES; +import static android.net.TrafficStats.GB_IN_BYTES; +import static android.net.TrafficStats.MB_IN_BYTES; +import static android.text.format.DateUtils.DAY_IN_MILLIS; +import static android.text.format.DateUtils.HOUR_IN_MILLIS; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; +import static android.text.format.DateUtils.SECOND_IN_MILLIS; +import static android.text.format.DateUtils.WEEK_IN_MILLIS; +import static android.text.format.DateUtils.YEAR_IN_MILLIS; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.util.Log; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.frameworks.tests.net.R; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.util.Random; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkStatsHistoryTest { + private static final String TAG = "NetworkStatsHistoryTest"; + + private static final long TEST_START = 1194220800000L; + + private NetworkStatsHistory stats; + + @After + public void tearDown() throws Exception { + if (stats != null) { + assertConsistent(stats); + } + } + + @Test + public void testReadOriginalVersion() throws Exception { + final Context context = InstrumentationRegistry.getContext(); + final DataInputStream in = + new DataInputStream(context.getResources().openRawResource(R.raw.history_v1)); + + NetworkStatsHistory.Entry entry = null; + try { + final NetworkStatsHistory history = new NetworkStatsHistory(in); + assertEquals(15 * SECOND_IN_MILLIS, history.getBucketDuration()); + + entry = history.getValues(0, entry); + assertEquals(29143L, entry.rxBytes); + assertEquals(6223L, entry.txBytes); + + entry = history.getValues(history.size() - 1, entry); + assertEquals(1476L, entry.rxBytes); + assertEquals(838L, entry.txBytes); + + entry = history.getValues(Long.MIN_VALUE, Long.MAX_VALUE, entry); + assertEquals(332401L, entry.rxBytes); + assertEquals(64314L, entry.txBytes); + + } finally { + in.close(); + } + } + + @Test + public void testRecordSingleBucket() throws Exception { + final long BUCKET_SIZE = HOUR_IN_MILLIS; + stats = new NetworkStatsHistory(BUCKET_SIZE); + + // record data into narrow window to get single bucket + stats.recordData(TEST_START, TEST_START + SECOND_IN_MILLIS, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L)); + + assertEquals(1, stats.size()); + assertValues(stats, 0, SECOND_IN_MILLIS, 1024L, 10L, 2048L, 20L, 2L); + } + + @Test + public void testRecordEqualBuckets() throws Exception { + final long bucketDuration = HOUR_IN_MILLIS; + stats = new NetworkStatsHistory(bucketDuration); + + // split equally across two buckets + final long recordStart = TEST_START + (bucketDuration / 2); + stats.recordData(recordStart, recordStart + bucketDuration, + new NetworkStats.Entry(1024L, 10L, 128L, 2L, 2L)); + + assertEquals(2, stats.size()); + assertValues(stats, 0, HOUR_IN_MILLIS / 2, 512L, 5L, 64L, 1L, 1L); + assertValues(stats, 1, HOUR_IN_MILLIS / 2, 512L, 5L, 64L, 1L, 1L); + } + + @Test + public void testRecordTouchingBuckets() throws Exception { + final long BUCKET_SIZE = 15 * MINUTE_IN_MILLIS; + stats = new NetworkStatsHistory(BUCKET_SIZE); + + // split almost completely into middle bucket, but with a few minutes + // overlap into neighboring buckets. total record is 20 minutes. + final long recordStart = (TEST_START + BUCKET_SIZE) - MINUTE_IN_MILLIS; + final long recordEnd = (TEST_START + (BUCKET_SIZE * 2)) + (MINUTE_IN_MILLIS * 4); + stats.recordData(recordStart, recordEnd, + new NetworkStats.Entry(1000L, 2000L, 5000L, 10000L, 100L)); + + assertEquals(3, stats.size()); + // first bucket should have (1/20 of value) + assertValues(stats, 0, MINUTE_IN_MILLIS, 50L, 100L, 250L, 500L, 5L); + // second bucket should have (15/20 of value) + assertValues(stats, 1, 15 * MINUTE_IN_MILLIS, 750L, 1500L, 3750L, 7500L, 75L); + // final bucket should have (4/20 of value) + assertValues(stats, 2, 4 * MINUTE_IN_MILLIS, 200L, 400L, 1000L, 2000L, 20L); + } + + @Test + public void testRecordGapBuckets() throws Exception { + final long BUCKET_SIZE = HOUR_IN_MILLIS; + stats = new NetworkStatsHistory(BUCKET_SIZE); + + // record some data today and next week with large gap + final long firstStart = TEST_START; + final long lastStart = TEST_START + WEEK_IN_MILLIS; + stats.recordData(firstStart, firstStart + SECOND_IN_MILLIS, + new NetworkStats.Entry(128L, 2L, 256L, 4L, 1L)); + stats.recordData(lastStart, lastStart + SECOND_IN_MILLIS, + new NetworkStats.Entry(64L, 1L, 512L, 8L, 2L)); + + // we should have two buckets, far apart from each other + assertEquals(2, stats.size()); + assertValues(stats, 0, SECOND_IN_MILLIS, 128L, 2L, 256L, 4L, 1L); + assertValues(stats, 1, SECOND_IN_MILLIS, 64L, 1L, 512L, 8L, 2L); + + // now record something in middle, spread across two buckets + final long middleStart = TEST_START + DAY_IN_MILLIS; + final long middleEnd = middleStart + (HOUR_IN_MILLIS * 2); + stats.recordData(middleStart, middleEnd, + new NetworkStats.Entry(2048L, 4L, 2048L, 4L, 2L)); + + // now should have four buckets, with new record in middle two buckets + assertEquals(4, stats.size()); + assertValues(stats, 0, SECOND_IN_MILLIS, 128L, 2L, 256L, 4L, 1L); + assertValues(stats, 1, HOUR_IN_MILLIS, 1024L, 2L, 1024L, 2L, 1L); + assertValues(stats, 2, HOUR_IN_MILLIS, 1024L, 2L, 1024L, 2L, 1L); + assertValues(stats, 3, SECOND_IN_MILLIS, 64L, 1L, 512L, 8L, 2L); + } + + @Test + public void testRecordOverlapBuckets() throws Exception { + final long BUCKET_SIZE = HOUR_IN_MILLIS; + stats = new NetworkStatsHistory(BUCKET_SIZE); + + // record some data in one bucket, and another overlapping buckets + stats.recordData(TEST_START, TEST_START + SECOND_IN_MILLIS, + new NetworkStats.Entry(256L, 2L, 256L, 2L, 1L)); + final long midStart = TEST_START + (HOUR_IN_MILLIS / 2); + stats.recordData(midStart, midStart + HOUR_IN_MILLIS, + new NetworkStats.Entry(1024L, 10L, 1024L, 10L, 10L)); + + // should have two buckets, with some data mixed together + assertEquals(2, stats.size()); + assertValues(stats, 0, SECOND_IN_MILLIS + (HOUR_IN_MILLIS / 2), 768L, 7L, 768L, 7L, 6L); + assertValues(stats, 1, (HOUR_IN_MILLIS / 2), 512L, 5L, 512L, 5L, 5L); + } + + @Test + public void testRecordEntireGapIdentical() throws Exception { + // first, create two separate histories far apart + final NetworkStatsHistory stats1 = new NetworkStatsHistory(HOUR_IN_MILLIS); + stats1.recordData(TEST_START, TEST_START + 2 * HOUR_IN_MILLIS, 2000L, 1000L); + + final long TEST_START_2 = TEST_START + DAY_IN_MILLIS; + final NetworkStatsHistory stats2 = new NetworkStatsHistory(HOUR_IN_MILLIS); + stats2.recordData(TEST_START_2, TEST_START_2 + 2 * HOUR_IN_MILLIS, 1000L, 500L); + + // combine together with identical bucket size + stats = new NetworkStatsHistory(HOUR_IN_MILLIS); + stats.recordEntireHistory(stats1); + stats.recordEntireHistory(stats2); + + // first verify that totals match up + assertValues(stats, TEST_START - WEEK_IN_MILLIS, TEST_START + WEEK_IN_MILLIS, 3000L, 1500L); + + // now inspect internal buckets + assertValues(stats, 0, 1000L, 500L); + assertValues(stats, 1, 1000L, 500L); + assertValues(stats, 2, 500L, 250L); + assertValues(stats, 3, 500L, 250L); + } + + @Test + public void testRecordEntireOverlapVaryingBuckets() throws Exception { + // create history just over hour bucket boundary + final NetworkStatsHistory stats1 = new NetworkStatsHistory(HOUR_IN_MILLIS); + stats1.recordData(TEST_START, TEST_START + MINUTE_IN_MILLIS * 60, 600L, 600L); + + final long TEST_START_2 = TEST_START + MINUTE_IN_MILLIS; + final NetworkStatsHistory stats2 = new NetworkStatsHistory(MINUTE_IN_MILLIS); + stats2.recordData(TEST_START_2, TEST_START_2 + MINUTE_IN_MILLIS * 5, 50L, 50L); + + // combine together with minute bucket size + stats = new NetworkStatsHistory(MINUTE_IN_MILLIS); + stats.recordEntireHistory(stats1); + stats.recordEntireHistory(stats2); + + // first verify that totals match up + assertValues(stats, TEST_START - WEEK_IN_MILLIS, TEST_START + WEEK_IN_MILLIS, 650L, 650L); + + // now inspect internal buckets + assertValues(stats, 0, 10L, 10L); + assertValues(stats, 1, 20L, 20L); + assertValues(stats, 2, 20L, 20L); + assertValues(stats, 3, 20L, 20L); + assertValues(stats, 4, 20L, 20L); + assertValues(stats, 5, 20L, 20L); + assertValues(stats, 6, 10L, 10L); + + // now combine using 15min buckets + stats = new NetworkStatsHistory(HOUR_IN_MILLIS / 4); + stats.recordEntireHistory(stats1); + stats.recordEntireHistory(stats2); + + // first verify that totals match up + assertValues(stats, TEST_START - WEEK_IN_MILLIS, TEST_START + WEEK_IN_MILLIS, 650L, 650L); + + // and inspect buckets + assertValues(stats, 0, 200L, 200L); + assertValues(stats, 1, 150L, 150L); + assertValues(stats, 2, 150L, 150L); + assertValues(stats, 3, 150L, 150L); + } + + @Test + public void testRemove() throws Exception { + stats = new NetworkStatsHistory(HOUR_IN_MILLIS); + + // record some data across 24 buckets + stats.recordData(TEST_START, TEST_START + DAY_IN_MILLIS, 24L, 24L); + assertEquals(24, stats.size()); + + // try removing invalid data; should be no change + stats.removeBucketsBefore(0 - DAY_IN_MILLIS); + assertEquals(24, stats.size()); + + // try removing far before buckets; should be no change + stats.removeBucketsBefore(TEST_START - YEAR_IN_MILLIS); + assertEquals(24, stats.size()); + + // try removing just moments into first bucket; should be no change + // since that bucket contains data beyond the cutoff + stats.removeBucketsBefore(TEST_START + SECOND_IN_MILLIS); + assertEquals(24, stats.size()); + + // try removing single bucket + stats.removeBucketsBefore(TEST_START + HOUR_IN_MILLIS); + assertEquals(23, stats.size()); + + // try removing multiple buckets + stats.removeBucketsBefore(TEST_START + (4 * HOUR_IN_MILLIS)); + assertEquals(20, stats.size()); + + // try removing all buckets + stats.removeBucketsBefore(TEST_START + YEAR_IN_MILLIS); + assertEquals(0, stats.size()); + } + + @Test + public void testTotalData() throws Exception { + final long BUCKET_SIZE = HOUR_IN_MILLIS; + stats = new NetworkStatsHistory(BUCKET_SIZE); + + // record uniform data across day + stats.recordData(TEST_START, TEST_START + DAY_IN_MILLIS, 2400L, 4800L); + + // verify that total outside range is 0 + assertValues(stats, TEST_START - WEEK_IN_MILLIS, TEST_START - DAY_IN_MILLIS, 0L, 0L); + + // verify total in first hour + assertValues(stats, TEST_START, TEST_START + HOUR_IN_MILLIS, 100L, 200L); + + // verify total across 1.5 hours + assertValues(stats, TEST_START, TEST_START + (long) (1.5 * HOUR_IN_MILLIS), 150L, 300L); + + // verify total beyond end + assertValues(stats, TEST_START + (23 * HOUR_IN_MILLIS), TEST_START + WEEK_IN_MILLIS, 100L, 200L); + + // verify everything total + assertValues(stats, TEST_START - WEEK_IN_MILLIS, TEST_START + WEEK_IN_MILLIS, 2400L, 4800L); + + } + + @Test + public void testFuzzing() throws Exception { + try { + // fuzzing with random events, looking for crashes + final NetworkStats.Entry entry = new NetworkStats.Entry(); + final Random r = new Random(); + for (int i = 0; i < 500; i++) { + stats = new NetworkStatsHistory(r.nextLong()); + for (int j = 0; j < 10000; j++) { + if (r.nextBoolean()) { + // add range + final long start = r.nextLong(); + final long end = start + r.nextInt(); + entry.rxBytes = nextPositiveLong(r); + entry.rxPackets = nextPositiveLong(r); + entry.txBytes = nextPositiveLong(r); + entry.txPackets = nextPositiveLong(r); + entry.operations = nextPositiveLong(r); + stats.recordData(start, end, entry); + } else { + // trim something + stats.removeBucketsBefore(r.nextLong()); + } + } + assertConsistent(stats); + } + } catch (Throwable e) { + Log.e(TAG, String.valueOf(stats)); + throw new RuntimeException(e); + } + } + + private static long nextPositiveLong(Random r) { + final long value = r.nextLong(); + return value < 0 ? -value : value; + } + + @Test + public void testIgnoreFields() throws Exception { + final NetworkStatsHistory history = new NetworkStatsHistory( + MINUTE_IN_MILLIS, 0, FIELD_RX_BYTES | FIELD_TX_BYTES); + + history.recordData(0, MINUTE_IN_MILLIS, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 4L)); + history.recordData(0, 2 * MINUTE_IN_MILLIS, + new NetworkStats.Entry(2L, 2L, 2L, 2L, 2L)); + + assertFullValues(history, UNKNOWN, 1026L, UNKNOWN, 2050L, UNKNOWN, UNKNOWN); + } + + @Test + public void testIgnoreFieldsRecordIn() throws Exception { + final NetworkStatsHistory full = new NetworkStatsHistory(MINUTE_IN_MILLIS, 0, FIELD_ALL); + final NetworkStatsHistory partial = new NetworkStatsHistory( + MINUTE_IN_MILLIS, 0, FIELD_RX_PACKETS | FIELD_OPERATIONS); + + full.recordData(0, MINUTE_IN_MILLIS, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 4L)); + partial.recordEntireHistory(full); + + assertFullValues(partial, UNKNOWN, UNKNOWN, 10L, UNKNOWN, UNKNOWN, 4L); + } + + @Test + public void testIgnoreFieldsRecordOut() throws Exception { + final NetworkStatsHistory full = new NetworkStatsHistory(MINUTE_IN_MILLIS, 0, FIELD_ALL); + final NetworkStatsHistory partial = new NetworkStatsHistory( + MINUTE_IN_MILLIS, 0, FIELD_RX_PACKETS | FIELD_OPERATIONS); + + partial.recordData(0, MINUTE_IN_MILLIS, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 4L)); + full.recordEntireHistory(partial); + + assertFullValues(full, MINUTE_IN_MILLIS, 0L, 10L, 0L, 0L, 4L); + } + + @Test + public void testSerialize() throws Exception { + final NetworkStatsHistory before = new NetworkStatsHistory(MINUTE_IN_MILLIS, 40, FIELD_ALL); + before.recordData(0, 4 * MINUTE_IN_MILLIS, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 4L)); + before.recordData(DAY_IN_MILLIS, DAY_IN_MILLIS + MINUTE_IN_MILLIS, + new NetworkStats.Entry(10L, 20L, 30L, 40L, 50L)); + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + before.writeToStream(new DataOutputStream(out)); + out.close(); + + final ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + final NetworkStatsHistory after = new NetworkStatsHistory(new DataInputStream(in)); + + // must have identical totals before and after + assertFullValues(before, 5 * MINUTE_IN_MILLIS, 1034L, 30L, 2078L, 60L, 54L); + assertFullValues(after, 5 * MINUTE_IN_MILLIS, 1034L, 30L, 2078L, 60L, 54L); + } + + @Test + public void testVarLong() throws Exception { + assertEquals(0L, performVarLong(0L)); + assertEquals(-1L, performVarLong(-1L)); + assertEquals(1024L, performVarLong(1024L)); + assertEquals(-1024L, performVarLong(-1024L)); + assertEquals(40 * MB_IN_BYTES, performVarLong(40 * MB_IN_BYTES)); + assertEquals(512 * GB_IN_BYTES, performVarLong(512 * GB_IN_BYTES)); + assertEquals(Long.MIN_VALUE, performVarLong(Long.MIN_VALUE)); + assertEquals(Long.MAX_VALUE, performVarLong(Long.MAX_VALUE)); + assertEquals(Long.MIN_VALUE + 40, performVarLong(Long.MIN_VALUE + 40)); + assertEquals(Long.MAX_VALUE - 40, performVarLong(Long.MAX_VALUE - 40)); + } + + @Test + public void testIndexBeforeAfter() throws Exception { + final long BUCKET_SIZE = HOUR_IN_MILLIS; + stats = new NetworkStatsHistory(BUCKET_SIZE); + + final long FIRST_START = TEST_START; + final long FIRST_END = FIRST_START + (2 * HOUR_IN_MILLIS); + final long SECOND_START = TEST_START + WEEK_IN_MILLIS; + final long SECOND_END = SECOND_START + HOUR_IN_MILLIS; + final long THIRD_START = TEST_START + (2 * WEEK_IN_MILLIS); + final long THIRD_END = THIRD_START + (2 * HOUR_IN_MILLIS); + + stats.recordData(FIRST_START, FIRST_END, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L)); + stats.recordData(SECOND_START, SECOND_END, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L)); + stats.recordData(THIRD_START, THIRD_END, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L)); + + // should have buckets: 2+1+2 + assertEquals(5, stats.size()); + + assertIndexBeforeAfter(stats, 0, 0, Long.MIN_VALUE); + assertIndexBeforeAfter(stats, 0, 1, FIRST_START); + assertIndexBeforeAfter(stats, 0, 1, FIRST_START + MINUTE_IN_MILLIS); + assertIndexBeforeAfter(stats, 0, 2, FIRST_START + HOUR_IN_MILLIS); + assertIndexBeforeAfter(stats, 1, 2, FIRST_START + HOUR_IN_MILLIS + MINUTE_IN_MILLIS); + assertIndexBeforeAfter(stats, 1, 2, FIRST_END - MINUTE_IN_MILLIS); + assertIndexBeforeAfter(stats, 1, 2, FIRST_END); + assertIndexBeforeAfter(stats, 1, 2, FIRST_END + MINUTE_IN_MILLIS); + assertIndexBeforeAfter(stats, 1, 2, SECOND_START - MINUTE_IN_MILLIS); + assertIndexBeforeAfter(stats, 1, 3, SECOND_START); + assertIndexBeforeAfter(stats, 2, 3, SECOND_END); + assertIndexBeforeAfter(stats, 2, 3, SECOND_END + MINUTE_IN_MILLIS); + assertIndexBeforeAfter(stats, 2, 3, THIRD_START - MINUTE_IN_MILLIS); + assertIndexBeforeAfter(stats, 2, 4, THIRD_START); + assertIndexBeforeAfter(stats, 3, 4, THIRD_START + MINUTE_IN_MILLIS); + assertIndexBeforeAfter(stats, 3, 4, THIRD_START + HOUR_IN_MILLIS); + assertIndexBeforeAfter(stats, 4, 4, THIRD_END); + assertIndexBeforeAfter(stats, 4, 4, THIRD_END + MINUTE_IN_MILLIS); + assertIndexBeforeAfter(stats, 4, 4, Long.MAX_VALUE); + } + + @Test + public void testIntersects() throws Exception { + final long BUCKET_SIZE = HOUR_IN_MILLIS; + stats = new NetworkStatsHistory(BUCKET_SIZE); + + final long FIRST_START = TEST_START; + final long FIRST_END = FIRST_START + (2 * HOUR_IN_MILLIS); + final long SECOND_START = TEST_START + WEEK_IN_MILLIS; + final long SECOND_END = SECOND_START + HOUR_IN_MILLIS; + final long THIRD_START = TEST_START + (2 * WEEK_IN_MILLIS); + final long THIRD_END = THIRD_START + (2 * HOUR_IN_MILLIS); + + stats.recordData(FIRST_START, FIRST_END, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L)); + stats.recordData(SECOND_START, SECOND_END, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L)); + stats.recordData(THIRD_START, THIRD_END, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L)); + + assertFalse(stats.intersects(10, 20)); + assertFalse(stats.intersects(TEST_START + YEAR_IN_MILLIS, TEST_START + YEAR_IN_MILLIS + 1)); + assertFalse(stats.intersects(Long.MAX_VALUE, Long.MIN_VALUE)); + + assertTrue(stats.intersects(Long.MIN_VALUE, Long.MAX_VALUE)); + assertTrue(stats.intersects(10, TEST_START + YEAR_IN_MILLIS)); + assertTrue(stats.intersects(TEST_START, TEST_START)); + assertTrue(stats.intersects(TEST_START + DAY_IN_MILLIS, TEST_START + DAY_IN_MILLIS + 1)); + assertTrue(stats.intersects(TEST_START + DAY_IN_MILLIS, Long.MAX_VALUE)); + assertTrue(stats.intersects(TEST_START + 1, Long.MAX_VALUE)); + + assertFalse(stats.intersects(Long.MIN_VALUE, TEST_START - 1)); + assertTrue(stats.intersects(Long.MIN_VALUE, TEST_START)); + assertTrue(stats.intersects(Long.MIN_VALUE, TEST_START + 1)); + } + + @Test + public void testSetValues() throws Exception { + stats = new NetworkStatsHistory(HOUR_IN_MILLIS); + stats.recordData(TEST_START, TEST_START + 1, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L)); + + assertEquals(1024L + 2048L, stats.getTotalBytes()); + + final NetworkStatsHistory.Entry entry = stats.getValues(0, null); + entry.rxBytes /= 2; + entry.txBytes *= 2; + stats.setValues(0, entry); + + assertEquals(512L + 4096L, stats.getTotalBytes()); + } + + private static void assertIndexBeforeAfter( + NetworkStatsHistory stats, int before, int after, long time) { + assertEquals("unexpected before", before, stats.getIndexBefore(time)); + assertEquals("unexpected after", after, stats.getIndexAfter(time)); + } + + private static long performVarLong(long before) throws Exception { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + writeVarLong(new DataOutputStream(out), before); + + final ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + return readVarLong(new DataInputStream(in)); + } + + private static void assertConsistent(NetworkStatsHistory stats) { + // verify timestamps are monotonic + long lastStart = Long.MIN_VALUE; + NetworkStatsHistory.Entry entry = null; + for (int i = 0; i < stats.size(); i++) { + entry = stats.getValues(i, entry); + assertTrue(lastStart < entry.bucketStart); + lastStart = entry.bucketStart; + } + } + + private static void assertValues( + NetworkStatsHistory stats, int index, long rxBytes, long txBytes) { + final NetworkStatsHistory.Entry entry = stats.getValues(index, null); + assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes); + assertEquals("unexpected txBytes", txBytes, entry.txBytes); + } + + private static void assertValues( + NetworkStatsHistory stats, long start, long end, long rxBytes, long txBytes) { + final NetworkStatsHistory.Entry entry = stats.getValues(start, end, null); + assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes); + assertEquals("unexpected txBytes", txBytes, entry.txBytes); + } + + private static void assertValues(NetworkStatsHistory stats, int index, long activeTime, + long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) { + final NetworkStatsHistory.Entry entry = stats.getValues(index, null); + assertEquals("unexpected activeTime", activeTime, entry.activeTime); + assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes); + assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets); + assertEquals("unexpected txBytes", txBytes, entry.txBytes); + assertEquals("unexpected txPackets", txPackets, entry.txPackets); + assertEquals("unexpected operations", operations, entry.operations); + } + + private static void assertFullValues(NetworkStatsHistory stats, long activeTime, long rxBytes, + long rxPackets, long txBytes, long txPackets, long operations) { + assertValues(stats, Long.MIN_VALUE, Long.MAX_VALUE, activeTime, rxBytes, rxPackets, txBytes, + txPackets, operations); + } + + private static void assertValues(NetworkStatsHistory stats, long start, long end, + long activeTime, long rxBytes, long rxPackets, long txBytes, long txPackets, + long operations) { + final NetworkStatsHistory.Entry entry = stats.getValues(start, end, null); + assertEquals("unexpected activeTime", activeTime, entry.activeTime); + assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes); + assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets); + assertEquals("unexpected txBytes", txBytes, entry.txBytes); + assertEquals("unexpected txPackets", txPackets, entry.txPackets); + assertEquals("unexpected operations", operations, entry.operations); + } +} diff --git a/tests/unit/java/android/net/NetworkStatsTest.java b/tests/unit/java/android/net/NetworkStatsTest.java new file mode 100644 index 0000000000..23d5a7e5d5 --- /dev/null +++ b/tests/unit/java/android/net/NetworkStatsTest.java @@ -0,0 +1,1024 @@ +/* + * Copyright (C) 2011 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; + +import static android.net.NetworkStats.DEFAULT_NETWORK_ALL; +import static android.net.NetworkStats.DEFAULT_NETWORK_NO; +import static android.net.NetworkStats.DEFAULT_NETWORK_YES; +import static android.net.NetworkStats.IFACE_ALL; +import static android.net.NetworkStats.INTERFACES_ALL; +import static android.net.NetworkStats.METERED_ALL; +import static android.net.NetworkStats.METERED_NO; +import static android.net.NetworkStats.METERED_YES; +import static android.net.NetworkStats.ROAMING_ALL; +import static android.net.NetworkStats.ROAMING_NO; +import static android.net.NetworkStats.ROAMING_YES; +import static android.net.NetworkStats.SET_ALL; +import static android.net.NetworkStats.SET_DBG_VPN_IN; +import static android.net.NetworkStats.SET_DBG_VPN_OUT; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.SET_FOREGROUND; +import static android.net.NetworkStats.TAG_ALL; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkStats.UID_ALL; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.os.Process; +import android.util.ArrayMap; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.google.android.collect.Sets; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.HashSet; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkStatsTest { + + private static final String TEST_IFACE = "test0"; + private static final String TEST_IFACE2 = "test2"; + private static final int TEST_UID = 1001; + private static final long TEST_START = 1194220800000L; + + @Test + public void testFindIndex() throws Exception { + final NetworkStats stats = new NetworkStats(TEST_START, 5) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 1024L, 8L, 0L, 0L, 10) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 1024L, 8L, 11) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 0L, 0L, 1024L, 8L, 11) + .insertEntry(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1024L, 8L, 1024L, 8L, 12) + .insertEntry(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_YES, + DEFAULT_NETWORK_YES, 1024L, 8L, 1024L, 8L, 12); + + assertEquals(4, stats.findIndex(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_YES, + ROAMING_YES, DEFAULT_NETWORK_YES)); + assertEquals(3, stats.findIndex(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO)); + assertEquals(2, stats.findIndex(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, + ROAMING_NO, DEFAULT_NETWORK_YES)); + assertEquals(1, stats.findIndex(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO)); + assertEquals(0, stats.findIndex(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_YES)); + assertEquals(-1, stats.findIndex(TEST_IFACE, 6, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO)); + assertEquals(-1, stats.findIndex(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO)); + } + + @Test + public void testFindIndexHinted() { + final NetworkStats stats = new NetworkStats(TEST_START, 3) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 1024L, 8L, 0L, 0L, 10) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 1024L, 8L, 11) + .insertEntry(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 1024L, 8L, 1024L, 8L, 12) + .insertEntry(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1024L, 8L, 0L, 0L, 10) + .insertEntry(TEST_IFACE2, 101, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 0L, 0L, 1024L, 8L, 11) + .insertEntry(TEST_IFACE2, 101, SET_DEFAULT, 0xF00D, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 1024L, 8L, 11) + .insertEntry(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 1024L, 8L, 1024L, 8L, 12) + .insertEntry(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_YES, + DEFAULT_NETWORK_NO, 1024L, 8L, 1024L, 8L, 12); + + // verify that we correctly find across regardless of hinting + for (int hint = 0; hint < stats.size(); hint++) { + assertEquals(0, stats.findIndexHinted(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, hint)); + assertEquals(1, stats.findIndexHinted(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, hint)); + assertEquals(2, stats.findIndexHinted(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, hint)); + assertEquals(3, stats.findIndexHinted(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, hint)); + assertEquals(4, stats.findIndexHinted(TEST_IFACE2, 101, SET_DEFAULT, 0xF00D, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, hint)); + assertEquals(5, stats.findIndexHinted(TEST_IFACE2, 101, SET_DEFAULT, 0xF00D, + METERED_YES, ROAMING_NO, DEFAULT_NETWORK_NO, hint)); + assertEquals(6, stats.findIndexHinted(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, hint)); + assertEquals(7, stats.findIndexHinted(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE, + METERED_YES, ROAMING_YES, DEFAULT_NETWORK_NO, hint)); + assertEquals(-1, stats.findIndexHinted(TEST_IFACE, 6, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, hint)); + assertEquals(-1, stats.findIndexHinted(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE, + METERED_YES, ROAMING_YES, DEFAULT_NETWORK_YES, hint)); + } + } + + @Test + public void testAddEntryGrow() throws Exception { + final NetworkStats stats = new NetworkStats(TEST_START, 4); + + assertEquals(0, stats.size()); + assertEquals(4, stats.internalSize()); + + stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 1L, 1L, 2L, 2L, 3); + stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 2L, 2L, 2L, 2L, 4); + stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES, + DEFAULT_NETWORK_YES, 3L, 3L, 2L, 2L, 5); + stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_YES, + DEFAULT_NETWORK_NO, 3L, 3L, 2L, 2L, 5); + + assertEquals(4, stats.size()); + assertEquals(4, stats.internalSize()); + + stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 4L, 40L, 4L, 40L, 7); + stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 5L, 50L, 4L, 40L, 8); + stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 6L, 60L, 5L, 50L, 10); + stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES, + DEFAULT_NETWORK_YES, 7L, 70L, 5L, 50L, 11); + stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_YES, + DEFAULT_NETWORK_NO, 7L, 70L, 5L, 50L, 11); + + assertEquals(9, stats.size()); + assertTrue(stats.internalSize() >= 9); + + assertValues(stats, 0, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 1L, 1L, 2L, 2L, 3); + assertValues(stats, 1, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 2L, 2L, 2L, 2L, 4); + assertValues(stats, 2, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES, + DEFAULT_NETWORK_YES, 3L, 3L, 2L, 2L, 5); + assertValues(stats, 3, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_YES, + ROAMING_YES, DEFAULT_NETWORK_NO, 3L, 3L, 2L, 2L, 5); + assertValues(stats, 4, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 4L, 40L, 4L, 40L, 7); + assertValues(stats, 5, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 5L, 50L, 4L, 40L, 8); + assertValues(stats, 6, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 6L, 60L, 5L, 50L, 10); + assertValues(stats, 7, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES, + DEFAULT_NETWORK_YES, 7L, 70L, 5L, 50L, 11); + assertValues(stats, 8, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_YES, + ROAMING_YES, DEFAULT_NETWORK_NO, 7L, 70L, 5L, 50L, 11); + } + + @Test + public void testCombineExisting() throws Exception { + final NetworkStats stats = new NetworkStats(TEST_START, 10); + + stats.insertEntry(TEST_IFACE, 1001, SET_DEFAULT, TAG_NONE, 512L, 4L, 256L, 2L, 10); + stats.insertEntry(TEST_IFACE, 1001, SET_DEFAULT, 0xff, 128L, 1L, 128L, 1L, 2); + stats.combineValues(TEST_IFACE, 1001, SET_DEFAULT, TAG_NONE, -128L, -1L, + -128L, -1L, -1); + + assertValues(stats, 0, TEST_IFACE, 1001, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 384L, 3L, 128L, 1L, 9); + assertValues(stats, 1, TEST_IFACE, 1001, SET_DEFAULT, 0xff, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 128L, 1L, 128L, 1L, 2); + + // now try combining that should create row + stats.combineValues(TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 3); + assertValues(stats, 2, TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 128L, 1L, 128L, 1L, 3); + stats.combineValues(TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 3); + assertValues(stats, 2, TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 256L, 2L, 256L, 2L, 6); + } + + @Test + public void testSubtractIdenticalData() throws Exception { + final NetworkStats before = new NetworkStats(TEST_START, 2) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 11) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 12); + + final NetworkStats after = new NetworkStats(TEST_START, 2) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 11) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 12); + + final NetworkStats result = after.subtract(before); + + // identical data should result in zero delta + assertValues(result, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0); + assertValues(result, 1, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0); + } + + @Test + public void testSubtractIdenticalRows() throws Exception { + final NetworkStats before = new NetworkStats(TEST_START, 2) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 11) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 12); + + final NetworkStats after = new NetworkStats(TEST_START, 2) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1025L, 9L, 2L, 1L, 15) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 3L, 1L, 1028L, 9L, 20); + + final NetworkStats result = after.subtract(before); + + // expect delta between measurements + assertValues(result, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1L, 1L, 2L, 1L, 4); + assertValues(result, 1, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 3L, 1L, 4L, 1L, 8); + } + + @Test + public void testSubtractNewRows() throws Exception { + final NetworkStats before = new NetworkStats(TEST_START, 2) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 11) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 12); + + final NetworkStats after = new NetworkStats(TEST_START, 3) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 11) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 12) + .insertEntry(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, 1024L, 8L, 1024L, 8L, 20); + + final NetworkStats result = after.subtract(before); + + // its okay to have new rows + assertValues(result, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0); + assertValues(result, 1, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0); + assertValues(result, 2, TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1024L, 8L, 1024L, 8L, 20); + } + + @Test + public void testSubtractMissingRows() throws Exception { + final NetworkStats before = new NetworkStats(TEST_START, 2) + .insertEntry(TEST_IFACE, UID_ALL, SET_DEFAULT, TAG_NONE, 1024L, 0L, 0L, 0L, 0) + .insertEntry(TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, 2048L, 0L, 0L, 0L, 0); + + final NetworkStats after = new NetworkStats(TEST_START, 1) + .insertEntry(TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, 2049L, 2L, 3L, 4L, 0); + + final NetworkStats result = after.subtract(before); + + // should silently drop omitted rows + assertEquals(1, result.size()); + assertValues(result, 0, TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 1L, 2L, 3L, 4L, 0); + assertEquals(4L, result.getTotalBytes()); + } + + @Test + public void testTotalBytes() throws Exception { + final NetworkStats iface = new NetworkStats(TEST_START, 2) + .insertEntry(TEST_IFACE, UID_ALL, SET_DEFAULT, TAG_NONE, 128L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, 256L, 0L, 0L, 0L, 0L); + assertEquals(384L, iface.getTotalBytes()); + + final NetworkStats uidSet = new NetworkStats(TEST_START, 3) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 32L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 32L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_FOREGROUND, TAG_NONE, 32L, 0L, 0L, 0L, 0L); + assertEquals(96L, uidSet.getTotalBytes()); + + final NetworkStats uidTag = new NetworkStats(TEST_START, 6) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 16L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, 16L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, 8L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, 16L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 16L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 8L, 0L, 0L, 0L, 0L); + assertEquals(64L, uidTag.getTotalBytes()); + + final NetworkStats uidMetered = new NetworkStats(TEST_START, 3) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_NO, 32L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L); + assertEquals(96L, uidMetered.getTotalBytes()); + + final NetworkStats uidRoaming = new NetworkStats(TEST_START, 3) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_NO, 32L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES, + DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L); + assertEquals(96L, uidRoaming.getTotalBytes()); + } + + @Test + public void testGroupedByIfaceEmpty() throws Exception { + final NetworkStats uidStats = new NetworkStats(TEST_START, 3); + final NetworkStats grouped = uidStats.groupedByIface(); + + assertEquals(0, uidStats.size()); + assertEquals(0, grouped.size()); + } + + @Test + public void testGroupedByIfaceAll() throws Exception { + final NetworkStats uidStats = new NetworkStats(TEST_START, 3) + .insertEntry(IFACE_ALL, 100, SET_ALL, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 8L, 0L, 2L, 20L) + .insertEntry(IFACE_ALL, 101, SET_FOREGROUND, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_NO, 128L, 8L, 0L, 2L, 20L) + .insertEntry(IFACE_ALL, 101, SET_ALL, TAG_NONE, METERED_NO, ROAMING_YES, + DEFAULT_NETWORK_YES, 128L, 8L, 0L, 2L, 20L); + final NetworkStats grouped = uidStats.groupedByIface(); + + assertEquals(3, uidStats.size()); + assertEquals(1, grouped.size()); + + assertValues(grouped, 0, IFACE_ALL, UID_ALL, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, + DEFAULT_NETWORK_ALL, 384L, 24L, 0L, 6L, 0L); + } + + @Test + public void testGroupedByIface() throws Exception { + final NetworkStats uidStats = new NetworkStats(TEST_START, 7) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 8L, 0L, 2L, 20L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 512L, 32L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 64L, 4L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 512L, 32L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 8L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_NO, 128L, 8L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES, + DEFAULT_NETWORK_YES, 128L, 8L, 0L, 0L, 0L); + + final NetworkStats grouped = uidStats.groupedByIface(); + + assertEquals(7, uidStats.size()); + + assertEquals(2, grouped.size()); + assertValues(grouped, 0, TEST_IFACE, UID_ALL, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, + DEFAULT_NETWORK_ALL, 384L, 24L, 0L, 2L, 0L); + assertValues(grouped, 1, TEST_IFACE2, UID_ALL, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, + DEFAULT_NETWORK_ALL, 1024L, 64L, 0L, 0L, 0L); + } + + @Test + public void testAddAllValues() { + final NetworkStats first = new NetworkStats(TEST_START, 5) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 32L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, METERED_YES, ROAMING_YES, + DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L); + + final NetworkStats second = new NetworkStats(TEST_START, 2) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 32L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, METERED_YES, ROAMING_YES, + DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L); + + first.combineAllValues(second); + + assertEquals(4, first.size()); + assertValues(first, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 64L, 0L, 0L, 0L, 0L); + assertValues(first, 1, TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 32L, 0L, 0L, 0L, 0L); + assertValues(first, 2, TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, METERED_YES, ROAMING_YES, + DEFAULT_NETWORK_YES, 64L, 0L, 0L, 0L, 0L); + assertValues(first, 3, TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 32L, 0L, 0L, 0L, 0L); + } + + @Test + public void testGetTotal() { + final NetworkStats stats = new NetworkStats(TEST_START, 7) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 8L, 0L, 2L, 20L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 512L, 32L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 64L, 4L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 512L,32L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 8L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 128L, 8L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES, + DEFAULT_NETWORK_NO, 128L, 8L, 0L, 0L, 0L); + + assertValues(stats.getTotal(null), 1408L, 88L, 0L, 2L, 20L); + assertValues(stats.getTotal(null, 100), 1280L, 80L, 0L, 2L, 20L); + assertValues(stats.getTotal(null, 101), 128L, 8L, 0L, 0L, 0L); + + final HashSet ifaces = Sets.newHashSet(); + assertValues(stats.getTotal(null, ifaces), 0L, 0L, 0L, 0L, 0L); + + ifaces.add(TEST_IFACE2); + assertValues(stats.getTotal(null, ifaces), 1024L, 64L, 0L, 0L, 0L); + } + + @Test + public void testRemoveUids() throws Exception { + final NetworkStats before = new NetworkStats(TEST_START, 3); + + // Test 0 item stats. + NetworkStats after = before.clone(); + after.removeUids(new int[0]); + assertEquals(0, after.size()); + after.removeUids(new int[] {100}); + assertEquals(0, after.size()); + + // Test 1 item stats. + before.insertEntry(TEST_IFACE, 99, SET_DEFAULT, TAG_NONE, 1L, 128L, 0L, 2L, 20L); + after = before.clone(); + after.removeUids(new int[0]); + assertEquals(1, after.size()); + assertValues(after, 0, TEST_IFACE, 99, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1L, 128L, 0L, 2L, 20L); + after.removeUids(new int[] {99}); + assertEquals(0, after.size()); + + // Append remaining test items. + before.insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 2L, 64L, 0L, 2L, 20L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, 4L, 32L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, 8L, 16L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, 16L, 8L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 32L, 4L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 64L, 2L, 0L, 0L, 0L); + assertEquals(7, before.size()); + + // Test remove with empty uid list. + after = before.clone(); + after.removeUids(new int[0]); + assertValues(after.getTotalIncludingTags(null), 127L, 254L, 0L, 4L, 40L); + + // Test remove uids don't exist in stats. + after.removeUids(new int[] {98, 0, Integer.MIN_VALUE, Integer.MAX_VALUE}); + assertValues(after.getTotalIncludingTags(null), 127L, 254L, 0L, 4L, 40L); + + // Test remove all uids. + after.removeUids(new int[] {99, 100, 100, 101}); + assertEquals(0, after.size()); + + // Test remove in the middle. + after = before.clone(); + after.removeUids(new int[] {100}); + assertEquals(3, after.size()); + assertValues(after, 0, TEST_IFACE, 99, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1L, 128L, 0L, 2L, 20L); + assertValues(after, 1, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 32L, 4L, 0L, 0L, 0L); + assertValues(after, 2, TEST_IFACE, 101, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 64L, 2L, 0L, 0L, 0L); + } + + @Test + public void testRemoveEmptyEntries() throws Exception { + // Test empty stats. + final NetworkStats statsEmpty = new NetworkStats(TEST_START, 3); + assertEquals(0, statsEmpty.removeEmptyEntries().size()); + + // Test stats with non-zero entry. + final NetworkStats statsNonZero = new NetworkStats(TEST_START, 1) + .insertEntry(TEST_IFACE, 99, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 1L, 128L, 0L, 2L, 20L); + assertEquals(1, statsNonZero.size()); + final NetworkStats expectedNonZero = statsNonZero.removeEmptyEntries(); + assertEquals(1, expectedNonZero.size()); + assertValues(expectedNonZero, 0, TEST_IFACE, 99, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 1L, 128L, 0L, 2L, 20L); + + // Test stats with empty entry. + final NetworkStats statsZero = new NetworkStats(TEST_START, 1) + .insertEntry(TEST_IFACE, 99, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L); + assertEquals(1, statsZero.size()); + final NetworkStats expectedZero = statsZero.removeEmptyEntries(); + assertEquals(1, statsZero.size()); // Assert immutable. + assertEquals(0, expectedZero.size()); + + // Test stats with multiple entries. + final NetworkStats statsMultiple = new NetworkStats(TEST_START, 0) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 2L, 64L, 0L, 2L, 20L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, 4L, 32L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 0L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 0L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, 8L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, 0L, 8L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 4L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 0L, 0L, 0L, 2L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 0L, 0L, 0L, 0L, 1L); + assertEquals(9, statsMultiple.size()); + final NetworkStats expectedMultiple = statsMultiple.removeEmptyEntries(); + assertEquals(9, statsMultiple.size()); // Assert immutable. + assertEquals(7, expectedMultiple.size()); + assertValues(expectedMultiple.getTotalIncludingTags(null), 14L, 104L, 4L, 4L, 21L); + + // Test stats with multiple empty entries. + assertEquals(statsMultiple.size(), statsMultiple.subtract(statsMultiple).size()); + assertEquals(0, statsMultiple.subtract(statsMultiple).removeEmptyEntries().size()); + } + + @Test + public void testClone() throws Exception { + final NetworkStats original = new NetworkStats(TEST_START, 5) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 128L, 8L, 0L, 2L, 20L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, 512L, 32L, 0L, 0L, 0L); + + // make clone and mutate original + final NetworkStats clone = original.clone(); + original.insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 128L, 8L, 0L, 0L, 0L); + + assertEquals(3, original.size()); + assertEquals(2, clone.size()); + + assertEquals(128L + 512L + 128L, original.getTotalBytes()); + assertEquals(128L + 512L, clone.getTotalBytes()); + } + + @Test + public void testAddWhenEmpty() throws Exception { + final NetworkStats red = new NetworkStats(TEST_START, -1); + final NetworkStats blue = new NetworkStats(TEST_START, 5) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 128L, 8L, 0L, 2L, 20L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, 512L, 32L, 0L, 0L, 0L); + + // We're mostly checking that we don't crash + red.combineAllValues(blue); + } + + @Test + public void testMigrateTun() throws Exception { + final int tunUid = 10030; + final String tunIface = "tun0"; + final String underlyingIface = "wlan0"; + final int testTag1 = 8888; + NetworkStats delta = new NetworkStats(TEST_START, 17) + .insertEntry(tunIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 39605L, 46L, 12259L, 55L, 0L) + .insertEntry(tunIface, 10100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L) + .insertEntry(tunIface, 10120, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 72667L, 197L, 43909L, 241L, 0L) + .insertEntry(tunIface, 10120, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 9297L, 17L, 4128L, 21L, 0L) + // VPN package also uses some traffic through unprotected network. + .insertEntry(tunIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 4983L, 10L, 1801L, 12L, 0L) + .insertEntry(tunIface, tunUid, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L) + // Tag entries + .insertEntry(tunIface, 10120, SET_DEFAULT, testTag1, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 21691L, 41L, 13820L, 51L, 0L) + .insertEntry(tunIface, 10120, SET_FOREGROUND, testTag1, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1281L, 2L, 665L, 2L, 0L) + // Irrelevant entries + .insertEntry(TEST_IFACE, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1685L, 5L, 2070L, 6L, 0L) + // Underlying Iface entries + .insertEntry(underlyingIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 5178L, 8L, 2139L, 11L, 0L) + .insertEntry(underlyingIface, 10100, SET_FOREGROUND, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L) + .insertEntry(underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 149873L, 287L, 59217L /* smaller than sum(tun0) */, + 299L /* smaller than sum(tun0) */, 0L) + .insertEntry(underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L); + + delta.migrateTun(tunUid, tunIface, Arrays.asList(underlyingIface)); + assertEquals(20, delta.size()); + + // tunIface and TEST_IFACE entries are not changed. + assertValues(delta, 0, tunIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 39605L, 46L, 12259L, 55L, 0L); + assertValues(delta, 1, tunIface, 10100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L); + assertValues(delta, 2, tunIface, 10120, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 72667L, 197L, 43909L, 241L, 0L); + assertValues(delta, 3, tunIface, 10120, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 9297L, 17L, 4128L, 21L, 0L); + assertValues(delta, 4, tunIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 4983L, 10L, 1801L, 12L, 0L); + assertValues(delta, 5, tunIface, tunUid, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L); + assertValues(delta, 6, tunIface, 10120, SET_DEFAULT, testTag1, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 21691L, 41L, 13820L, 51L, 0L); + assertValues(delta, 7, tunIface, 10120, SET_FOREGROUND, testTag1, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1281L, 2L, 665L, 2L, 0L); + assertValues(delta, 8, TEST_IFACE, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1685L, 5L, 2070L, 6L, 0L); + + // Existing underlying Iface entries are updated + assertValues(delta, 9, underlyingIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 44783L, 54L, 14178L, 62L, 0L); + assertValues(delta, 10, underlyingIface, 10100, SET_FOREGROUND, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L); + + // VPN underlying Iface entries are updated + assertValues(delta, 11, underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 28304L, 27L, 1L, 2L, 0L); + assertValues(delta, 12, underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L); + + // New entries are added for new application's underlying Iface traffic + assertContains(delta, underlyingIface, 10120, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 72667L, 197L, 43123L, 227L, 0L); + assertContains(delta, underlyingIface, 10120, SET_FOREGROUND, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 9297L, 17L, 4054, 19L, 0L); + assertContains(delta, underlyingIface, 10120, SET_DEFAULT, testTag1, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 21691L, 41L, 13572L, 48L, 0L); + assertContains(delta, underlyingIface, 10120, SET_FOREGROUND, testTag1, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 1281L, 2L, 653L, 1L, 0L); + + // New entries are added for debug purpose + assertContains(delta, underlyingIface, 10100, SET_DBG_VPN_IN, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 39605L, 46L, 12039, 51, 0); + assertContains(delta, underlyingIface, 10120, SET_DBG_VPN_IN, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 81964, 214, 47177, 246, 0); + assertContains(delta, underlyingIface, tunUid, SET_DBG_VPN_OUT, TAG_NONE, METERED_ALL, + ROAMING_ALL, DEFAULT_NETWORK_ALL, 121569, 260, 59216, 297, 0); + + } + + // Tests a case where all of the data received by the tun0 interface is echo back into the tun0 + // interface by the vpn app before it's sent out of the underlying interface. The VPN app should + // not be charged for the echoed data but it should still be charged for any extra data it sends + // via the underlying interface. + @Test + public void testMigrateTun_VpnAsLoopback() { + final int tunUid = 10030; + final String tunIface = "tun0"; + final String underlyingIface = "wlan0"; + NetworkStats delta = new NetworkStats(TEST_START, 9) + // 2 different apps sent/receive data via tun0. + .insertEntry(tunIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L) + .insertEntry(tunIface, 20100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 500L, 2L, 200L, 5L, 0L) + // VPN package resends data through the tunnel (with exaggerated overhead) + .insertEntry(tunIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 240000, 100L, 120000L, 60L, 0L) + // 1 app already has some traffic on the underlying interface, the other doesn't yet + .insertEntry(underlyingIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1000L, 10L, 2000L, 20L, 0L) + // Traffic through the underlying interface via the vpn app. + // This test should redistribute this data correctly. + .insertEntry(underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 75500L, 37L, 130000L, 70L, 0L); + + delta.migrateTun(tunUid, tunIface, Arrays.asList(underlyingIface)); + assertEquals(9, delta.size()); + + // tunIface entries should not be changed. + assertValues(delta, 0, tunIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + assertValues(delta, 1, tunIface, 20100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 500L, 2L, 200L, 5L, 0L); + assertValues(delta, 2, tunIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 240000L, 100L, 120000L, 60L, 0L); + + // Existing underlying Iface entries are updated + assertValues(delta, 3, underlyingIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 51000L, 35L, 102000L, 70L, 0L); + + // VPN underlying Iface entries are updated + assertValues(delta, 4, underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 25000L, 10L, 29800L, 15L, 0L); + + // New entries are added for new application's underlying Iface traffic + assertContains(delta, underlyingIface, 20100, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 500L, 2L, 200L, 5L, 0L); + + // New entries are added for debug purpose + assertContains(delta, underlyingIface, 10100, SET_DBG_VPN_IN, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + assertContains(delta, underlyingIface, 20100, SET_DBG_VPN_IN, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 500, 2L, 200L, 5L, 0L); + assertContains(delta, underlyingIface, tunUid, SET_DBG_VPN_OUT, TAG_NONE, METERED_ALL, + ROAMING_ALL, DEFAULT_NETWORK_ALL, 50500L, 27L, 100200L, 55, 0); + } + + @Test + public void testFilter_NoFilter() { + NetworkStats.Entry entry1 = new NetworkStats.Entry( + "test1", 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry2 = new NetworkStats.Entry( + "test2", 10101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry3 = new NetworkStats.Entry( + "test2", 10101, SET_DEFAULT, 123, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats stats = new NetworkStats(TEST_START, 3) + .insertEntry(entry1) + .insertEntry(entry2) + .insertEntry(entry3); + + stats.filter(UID_ALL, INTERFACES_ALL, TAG_ALL); + assertEquals(3, stats.size()); + assertEquals(entry1, stats.getValues(0, null)); + assertEquals(entry2, stats.getValues(1, null)); + assertEquals(entry3, stats.getValues(2, null)); + } + + @Test + public void testFilter_UidFilter() { + final int testUid = 10101; + NetworkStats.Entry entry1 = new NetworkStats.Entry( + "test1", 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry2 = new NetworkStats.Entry( + "test2", testUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry3 = new NetworkStats.Entry( + "test2", testUid, SET_DEFAULT, 123, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats stats = new NetworkStats(TEST_START, 3) + .insertEntry(entry1) + .insertEntry(entry2) + .insertEntry(entry3); + + stats.filter(testUid, INTERFACES_ALL, TAG_ALL); + assertEquals(2, stats.size()); + assertEquals(entry2, stats.getValues(0, null)); + assertEquals(entry3, stats.getValues(1, null)); + } + + @Test + public void testFilter_InterfaceFilter() { + final String testIf1 = "testif1"; + final String testIf2 = "testif2"; + NetworkStats.Entry entry1 = new NetworkStats.Entry( + testIf1, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry2 = new NetworkStats.Entry( + "otherif", 10101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry3 = new NetworkStats.Entry( + testIf1, 10101, SET_DEFAULT, 123, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry4 = new NetworkStats.Entry( + testIf2, 10101, SET_DEFAULT, 123, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats stats = new NetworkStats(TEST_START, 4) + .insertEntry(entry1) + .insertEntry(entry2) + .insertEntry(entry3) + .insertEntry(entry4); + + stats.filter(UID_ALL, new String[] { testIf1, testIf2 }, TAG_ALL); + assertEquals(3, stats.size()); + assertEquals(entry1, stats.getValues(0, null)); + assertEquals(entry3, stats.getValues(1, null)); + assertEquals(entry4, stats.getValues(2, null)); + } + + @Test + public void testFilter_EmptyInterfaceFilter() { + NetworkStats.Entry entry1 = new NetworkStats.Entry( + "if1", 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry2 = new NetworkStats.Entry( + "if2", 10101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats stats = new NetworkStats(TEST_START, 3) + .insertEntry(entry1) + .insertEntry(entry2); + + stats.filter(UID_ALL, new String[] { }, TAG_ALL); + assertEquals(0, stats.size()); + } + + @Test + public void testFilter_TagFilter() { + final int testTag = 123; + final int otherTag = 456; + NetworkStats.Entry entry1 = new NetworkStats.Entry( + "test1", 10100, SET_DEFAULT, testTag, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry2 = new NetworkStats.Entry( + "test2", 10101, SET_DEFAULT, testTag, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry3 = new NetworkStats.Entry( + "test2", 10101, SET_DEFAULT, otherTag, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats stats = new NetworkStats(TEST_START, 3) + .insertEntry(entry1) + .insertEntry(entry2) + .insertEntry(entry3); + + stats.filter(UID_ALL, INTERFACES_ALL, testTag); + assertEquals(2, stats.size()); + assertEquals(entry1, stats.getValues(0, null)); + assertEquals(entry2, stats.getValues(1, null)); + } + + @Test + public void testFilterDebugEntries() { + NetworkStats.Entry entry1 = new NetworkStats.Entry( + "test1", 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry2 = new NetworkStats.Entry( + "test2", 10101, SET_DBG_VPN_IN, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry3 = new NetworkStats.Entry( + "test2", 10101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry4 = new NetworkStats.Entry( + "test2", 10101, SET_DBG_VPN_OUT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats stats = new NetworkStats(TEST_START, 4) + .insertEntry(entry1) + .insertEntry(entry2) + .insertEntry(entry3) + .insertEntry(entry4); + + stats.filterDebugEntries(); + + assertEquals(2, stats.size()); + assertEquals(entry1, stats.getValues(0, null)); + assertEquals(entry3, stats.getValues(1, null)); + } + + @Test + public void testApply464xlatAdjustments() { + final String v4Iface = "v4-wlan0"; + final String baseIface = "wlan0"; + final String otherIface = "other"; + final int appUid = 10001; + final int rootUid = Process.ROOT_UID; + ArrayMap stackedIface = new ArrayMap<>(); + stackedIface.put(v4Iface, baseIface); + + // Ipv4 traffic sent/received by an app on stacked interface. + final NetworkStats.Entry appEntry = new NetworkStats.Entry( + v4Iface, appUid, SET_DEFAULT, TAG_NONE, + 30501490 /* rxBytes */, + 22401 /* rxPackets */, + 876235 /* txBytes */, + 13805 /* txPackets */, + 0 /* operations */); + + // Traffic measured for the root uid on the base interface. + final NetworkStats.Entry rootUidEntry = new NetworkStats.Entry( + baseIface, rootUid, SET_DEFAULT, TAG_NONE, + 163577 /* rxBytes */, + 187 /* rxPackets */, + 17607 /* txBytes */, + 97 /* txPackets */, + 0 /* operations */); + + final NetworkStats.Entry otherEntry = new NetworkStats.Entry( + otherIface, appUid, SET_DEFAULT, TAG_NONE, + 2600 /* rxBytes */, + 2 /* rxPackets */, + 3800 /* txBytes */, + 3 /* txPackets */, + 0 /* operations */); + + final NetworkStats stats = new NetworkStats(TEST_START, 3) + .insertEntry(appEntry) + .insertEntry(rootUidEntry) + .insertEntry(otherEntry); + + stats.apply464xlatAdjustments(stackedIface); + + assertEquals(3, stats.size()); + final NetworkStats.Entry expectedAppUid = new NetworkStats.Entry( + v4Iface, appUid, SET_DEFAULT, TAG_NONE, + 30949510, + 22401, + 1152335, + 13805, + 0); + final NetworkStats.Entry expectedRootUid = new NetworkStats.Entry( + baseIface, 0, SET_DEFAULT, TAG_NONE, + 163577, + 187, + 17607, + 97, + 0); + assertEquals(expectedAppUid, stats.getValues(0, null)); + assertEquals(expectedRootUid, stats.getValues(1, null)); + assertEquals(otherEntry, stats.getValues(2, null)); + } + + @Test + public void testApply464xlatAdjustments_noStackedIface() { + NetworkStats.Entry firstEntry = new NetworkStats.Entry( + "if1", 10002, SET_DEFAULT, TAG_NONE, + 2600 /* rxBytes */, + 2 /* rxPackets */, + 3800 /* txBytes */, + 3 /* txPackets */, + 0 /* operations */); + NetworkStats.Entry secondEntry = new NetworkStats.Entry( + "if2", 10002, SET_DEFAULT, TAG_NONE, + 5000 /* rxBytes */, + 3 /* rxPackets */, + 6000 /* txBytes */, + 4 /* txPackets */, + 0 /* operations */); + + NetworkStats stats = new NetworkStats(TEST_START, 2) + .insertEntry(firstEntry) + .insertEntry(secondEntry); + + // Empty map: no adjustment + stats.apply464xlatAdjustments(new ArrayMap<>()); + + assertEquals(2, stats.size()); + assertEquals(firstEntry, stats.getValues(0, null)); + assertEquals(secondEntry, stats.getValues(1, null)); + } + + private static void assertContains(NetworkStats stats, String iface, int uid, int set, + int tag, int metered, int roaming, int defaultNetwork, long rxBytes, long rxPackets, + long txBytes, long txPackets, long operations) { + int index = stats.findIndex(iface, uid, set, tag, metered, roaming, defaultNetwork); + assertTrue(index != -1); + assertValues(stats, index, iface, uid, set, tag, metered, roaming, defaultNetwork, + rxBytes, rxPackets, txBytes, txPackets, operations); + } + + private static void assertValues(NetworkStats stats, int index, String iface, int uid, int set, + int tag, int metered, int roaming, int defaultNetwork, long rxBytes, long rxPackets, + long txBytes, long txPackets, long operations) { + final NetworkStats.Entry entry = stats.getValues(index, null); + assertValues(entry, iface, uid, set, tag, metered, roaming, defaultNetwork); + assertValues(entry, rxBytes, rxPackets, txBytes, txPackets, operations); + } + + private static void assertValues( + NetworkStats.Entry entry, String iface, int uid, int set, int tag, int metered, + int roaming, int defaultNetwork) { + assertEquals(iface, entry.iface); + assertEquals(uid, entry.uid); + assertEquals(set, entry.set); + assertEquals(tag, entry.tag); + assertEquals(metered, entry.metered); + assertEquals(roaming, entry.roaming); + assertEquals(defaultNetwork, entry.defaultNetwork); + } + + private static void assertValues(NetworkStats.Entry entry, long rxBytes, long rxPackets, + long txBytes, long txPackets, long operations) { + assertEquals(rxBytes, entry.rxBytes); + assertEquals(rxPackets, entry.rxPackets); + assertEquals(txBytes, entry.txBytes); + assertEquals(txPackets, entry.txPackets); + assertEquals(operations, entry.operations); + } + +} diff --git a/tests/unit/java/android/net/NetworkTemplateTest.kt b/tests/unit/java/android/net/NetworkTemplateTest.kt new file mode 100644 index 0000000000..ab6b2f4098 --- /dev/null +++ b/tests/unit/java/android/net/NetworkTemplateTest.kt @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2020 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 + +import android.content.Context +import android.net.ConnectivityManager.TYPE_MOBILE +import android.net.ConnectivityManager.TYPE_WIFI +import android.net.NetworkIdentity.SUBTYPE_COMBINED +import android.net.NetworkIdentity.OEM_NONE +import android.net.NetworkIdentity.OEM_PAID +import android.net.NetworkIdentity.OEM_PRIVATE +import android.net.NetworkIdentity.buildNetworkIdentity +import android.net.NetworkStats.DEFAULT_NETWORK_ALL +import android.net.NetworkStats.METERED_ALL +import android.net.NetworkStats.ROAMING_ALL +import android.net.NetworkTemplate.MATCH_MOBILE +import android.net.NetworkTemplate.MATCH_MOBILE_WILDCARD +import android.net.NetworkTemplate.MATCH_WIFI +import android.net.NetworkTemplate.MATCH_WIFI_WILDCARD +import android.net.NetworkTemplate.WIFI_NETWORKID_ALL +import android.net.NetworkTemplate.NETWORK_TYPE_5G_NSA +import android.net.NetworkTemplate.NETWORK_TYPE_ALL +import android.net.NetworkTemplate.OEM_MANAGED_ALL +import android.net.NetworkTemplate.OEM_MANAGED_NO +import android.net.NetworkTemplate.OEM_MANAGED_YES +import android.net.NetworkTemplate.SUBSCRIBER_ID_MATCH_RULE_EXACT +import android.net.NetworkTemplate.buildTemplateWifi +import android.net.NetworkTemplate.buildTemplateWifiWildcard +import android.net.NetworkTemplate.buildTemplateCarrier +import android.net.NetworkTemplate.buildTemplateMobileWithRatType +import android.telephony.TelephonyManager +import com.android.testutils.assertParcelSane +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +private const val TEST_IMSI1 = "imsi1" +private const val TEST_IMSI2 = "imsi2" +private const val TEST_SSID1 = "ssid1" +private const val TEST_SSID2 = "ssid2" + +@RunWith(JUnit4::class) +class NetworkTemplateTest { + private val mockContext = mock(Context::class.java) + + private fun buildMobileNetworkState(subscriberId: String): NetworkStateSnapshot = + buildNetworkState(TYPE_MOBILE, subscriberId = subscriberId) + private fun buildWifiNetworkState(subscriberId: String?, ssid: String?): NetworkStateSnapshot = + buildNetworkState(TYPE_WIFI, subscriberId = subscriberId, ssid = ssid) + + private fun buildNetworkState( + type: Int, + subscriberId: String? = null, + ssid: String? = null, + oemManaged: Int = OEM_NONE + ): NetworkStateSnapshot { + val lp = LinkProperties() + val caps = NetworkCapabilities().apply { + setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, false) + setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, true) + setSSID(ssid) + setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID, + (oemManaged and OEM_PAID) == OEM_PAID) + setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE, + (oemManaged and OEM_PRIVATE) == OEM_PRIVATE) + } + return NetworkStateSnapshot(mock(Network::class.java), caps, lp, subscriberId, type) + } + + private fun NetworkTemplate.assertMatches(ident: NetworkIdentity) = + assertTrue(matches(ident), "$this does not match $ident") + + private fun NetworkTemplate.assertDoesNotMatch(ident: NetworkIdentity) = + assertFalse(matches(ident), "$this should match $ident") + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun testWifiWildcardMatches() { + val templateWifiWildcard = buildTemplateWifiWildcard() + + val identMobileImsi1 = buildNetworkIdentity(mockContext, + buildMobileNetworkState(TEST_IMSI1), + false, TelephonyManager.NETWORK_TYPE_UMTS) + val identWifiImsiNullSsid1 = buildNetworkIdentity( + mockContext, buildWifiNetworkState(null, TEST_SSID1), true, 0) + val identWifiImsi1Ssid1 = buildNetworkIdentity( + mockContext, buildWifiNetworkState(TEST_IMSI1, TEST_SSID1), true, 0) + + templateWifiWildcard.assertDoesNotMatch(identMobileImsi1) + templateWifiWildcard.assertMatches(identWifiImsiNullSsid1) + templateWifiWildcard.assertMatches(identWifiImsi1Ssid1) + } + + @Test + fun testWifiMatches() { + val templateWifiSsid1 = buildTemplateWifi(TEST_SSID1) + val templateWifiSsid1ImsiNull = buildTemplateWifi(TEST_SSID1, null) + val templateWifiSsid1Imsi1 = buildTemplateWifi(TEST_SSID1, TEST_IMSI1) + val templateWifiSsidAllImsi1 = buildTemplateWifi(WIFI_NETWORKID_ALL, TEST_IMSI1) + + val identMobile1 = buildNetworkIdentity(mockContext, buildMobileNetworkState(TEST_IMSI1), + false, TelephonyManager.NETWORK_TYPE_UMTS) + val identWifiImsiNullSsid1 = buildNetworkIdentity( + mockContext, buildWifiNetworkState(null, TEST_SSID1), true, 0) + val identWifiImsi1Ssid1 = buildNetworkIdentity( + mockContext, buildWifiNetworkState(TEST_IMSI1, TEST_SSID1), true, 0) + val identWifiImsi2Ssid1 = buildNetworkIdentity( + mockContext, buildWifiNetworkState(TEST_IMSI2, TEST_SSID1), true, 0) + val identWifiImsi1Ssid2 = buildNetworkIdentity( + mockContext, buildWifiNetworkState(TEST_IMSI1, TEST_SSID2), true, 0) + + // Verify that template with SSID only matches any subscriberId and specific SSID. + templateWifiSsid1.assertDoesNotMatch(identMobile1) + templateWifiSsid1.assertMatches(identWifiImsiNullSsid1) + templateWifiSsid1.assertMatches(identWifiImsi1Ssid1) + templateWifiSsid1.assertMatches(identWifiImsi2Ssid1) + templateWifiSsid1.assertDoesNotMatch(identWifiImsi1Ssid2) + + // Verify that template with SSID1 and null imsi matches any network with + // SSID1 and null imsi. + templateWifiSsid1ImsiNull.assertDoesNotMatch(identMobile1) + templateWifiSsid1ImsiNull.assertMatches(identWifiImsiNullSsid1) + templateWifiSsid1ImsiNull.assertDoesNotMatch(identWifiImsi1Ssid1) + templateWifiSsid1ImsiNull.assertDoesNotMatch(identWifiImsi2Ssid1) + templateWifiSsid1ImsiNull.assertDoesNotMatch(identWifiImsi1Ssid2) + + // Verify that template with SSID1 and imsi1 matches any network with + // SSID1 and imsi1. + templateWifiSsid1Imsi1.assertDoesNotMatch(identMobile1) + templateWifiSsid1Imsi1.assertDoesNotMatch(identWifiImsiNullSsid1) + templateWifiSsid1Imsi1.assertMatches(identWifiImsi1Ssid1) + templateWifiSsid1Imsi1.assertDoesNotMatch(identWifiImsi2Ssid1) + templateWifiSsid1Imsi1.assertDoesNotMatch(identWifiImsi1Ssid2) + + // Verify that template with SSID all and imsi1 matches any network with + // any SSID and imsi1. + templateWifiSsidAllImsi1.assertDoesNotMatch(identMobile1) + templateWifiSsidAllImsi1.assertDoesNotMatch(identWifiImsiNullSsid1) + templateWifiSsidAllImsi1.assertMatches(identWifiImsi1Ssid1) + templateWifiSsidAllImsi1.assertDoesNotMatch(identWifiImsi2Ssid1) + templateWifiSsidAllImsi1.assertMatches(identWifiImsi1Ssid2) + } + + @Test + fun testCarrierMatches() { + val templateCarrierImsi1 = buildTemplateCarrier(TEST_IMSI1) + + val identMobile1 = buildNetworkIdentity(mockContext, buildMobileNetworkState(TEST_IMSI1), + false, TelephonyManager.NETWORK_TYPE_UMTS) + val identMobile2 = buildNetworkIdentity(mockContext, buildMobileNetworkState(TEST_IMSI2), + false, TelephonyManager.NETWORK_TYPE_UMTS) + val identWifiSsid1 = buildNetworkIdentity( + mockContext, buildWifiNetworkState(null, TEST_SSID1), true, 0) + val identCarrierWifiImsi1 = buildNetworkIdentity( + mockContext, buildWifiNetworkState(TEST_IMSI1, TEST_SSID1), true, 0) + val identCarrierWifiImsi2 = buildNetworkIdentity( + mockContext, buildWifiNetworkState(TEST_IMSI2, TEST_SSID1), true, 0) + + templateCarrierImsi1.assertMatches(identCarrierWifiImsi1) + templateCarrierImsi1.assertDoesNotMatch(identCarrierWifiImsi2) + templateCarrierImsi1.assertDoesNotMatch(identWifiSsid1) + templateCarrierImsi1.assertMatches(identMobile1) + templateCarrierImsi1.assertDoesNotMatch(identMobile2) + } + + @Test + fun testRatTypeGroupMatches() { + val stateMobile = buildMobileNetworkState(TEST_IMSI1) + // Build UMTS template that matches mobile identities with RAT in the same + // group with any IMSI. See {@link NetworkTemplate#getCollapsedRatType}. + val templateUmts = buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UMTS) + // Build normal template that matches mobile identities with any RAT and IMSI. + val templateAll = buildTemplateMobileWithRatType(null, NETWORK_TYPE_ALL) + // Build template with UNKNOWN RAT that matches mobile identities with RAT that + // cannot be determined. + val templateUnknown = + buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UNKNOWN) + + val identUmts = buildNetworkIdentity( + mockContext, stateMobile, false, TelephonyManager.NETWORK_TYPE_UMTS) + val identHsdpa = buildNetworkIdentity( + mockContext, stateMobile, false, TelephonyManager.NETWORK_TYPE_HSDPA) + val identLte = buildNetworkIdentity( + mockContext, stateMobile, false, TelephonyManager.NETWORK_TYPE_LTE) + val identCombined = buildNetworkIdentity( + mockContext, stateMobile, false, SUBTYPE_COMBINED) + val identImsi2 = buildNetworkIdentity(mockContext, buildMobileNetworkState(TEST_IMSI2), + false, TelephonyManager.NETWORK_TYPE_UMTS) + val identWifi = buildNetworkIdentity( + mockContext, buildWifiNetworkState(null, TEST_SSID1), true, 0) + + // Assert that identity with the same RAT matches. + templateUmts.assertMatches(identUmts) + templateAll.assertMatches(identUmts) + templateUnknown.assertDoesNotMatch(identUmts) + // Assert that identity with the RAT within the same group matches. + templateUmts.assertMatches(identHsdpa) + templateAll.assertMatches(identHsdpa) + templateUnknown.assertDoesNotMatch(identHsdpa) + // Assert that identity with the RAT out of the same group only matches template with + // NETWORK_TYPE_ALL. + templateUmts.assertDoesNotMatch(identLte) + templateAll.assertMatches(identLte) + templateUnknown.assertDoesNotMatch(identLte) + // Assert that identity with combined RAT only matches with template with NETWORK_TYPE_ALL + // and NETWORK_TYPE_UNKNOWN. + templateUmts.assertDoesNotMatch(identCombined) + templateAll.assertMatches(identCombined) + templateUnknown.assertMatches(identCombined) + // Assert that identity with different IMSI matches. + templateUmts.assertMatches(identImsi2) + templateAll.assertMatches(identImsi2) + templateUnknown.assertDoesNotMatch(identImsi2) + // Assert that wifi identity does not match. + templateUmts.assertDoesNotMatch(identWifi) + templateAll.assertDoesNotMatch(identWifi) + templateUnknown.assertDoesNotMatch(identWifi) + } + + @Test + fun testParcelUnparcel() { + val templateMobile = NetworkTemplate(MATCH_MOBILE, TEST_IMSI1, null, null, METERED_ALL, + ROAMING_ALL, DEFAULT_NETWORK_ALL, TelephonyManager.NETWORK_TYPE_LTE, + OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT) + val templateWifi = NetworkTemplate(MATCH_WIFI, null, null, TEST_SSID1, METERED_ALL, + ROAMING_ALL, DEFAULT_NETWORK_ALL, 0, OEM_MANAGED_ALL, + SUBSCRIBER_ID_MATCH_RULE_EXACT) + val templateOem = NetworkTemplate(MATCH_MOBILE, null, null, null, METERED_ALL, + ROAMING_ALL, DEFAULT_NETWORK_ALL, 0, OEM_MANAGED_YES, + SUBSCRIBER_ID_MATCH_RULE_EXACT) + assertParcelSane(templateMobile, 10) + assertParcelSane(templateWifi, 10) + assertParcelSane(templateOem, 10) + } + + // Verify NETWORK_TYPE_* constants in NetworkTemplate do not conflict with + // TelephonyManager#NETWORK_TYPE_* constants. + @Test + fun testNetworkTypeConstants() { + for (ratType in TelephonyManager.getAllNetworkTypes()) { + assertNotEquals(NETWORK_TYPE_ALL, ratType) + assertNotEquals(NETWORK_TYPE_5G_NSA, ratType) + } + } + + @Test + fun testOemNetworkConstants() { + val constantValues = arrayOf(OEM_MANAGED_YES, OEM_MANAGED_ALL, OEM_MANAGED_NO, + OEM_PAID, OEM_PRIVATE, OEM_PAID or OEM_PRIVATE) + + // Verify that "not OEM managed network" constants are equal. + assertEquals(OEM_MANAGED_NO, OEM_NONE) + + // Verify the constants don't conflict. + assertEquals(constantValues.size, constantValues.distinct().count()) + } + + /** + * Helper to enumerate and assert OEM managed wifi and mobile {@code NetworkTemplate}s match + * their the appropriate OEM managed {@code NetworkIdentity}s. + * + * @param networkType {@code TYPE_MOBILE} or {@code TYPE_WIFI} + * @param matchType A match rule from {@code NetworkTemplate.MATCH_*} corresponding to the + * networkType. + * @param subscriberId To be populated with {@code TEST_IMSI*} only if networkType is + * {@code TYPE_MOBILE}. May be left as null when matchType is + * {@link NetworkTemplate.MATCH_MOBILE_WILDCARD}. + * @param templateSsid Top be populated with {@code TEST_SSID*} only if networkType is + * {@code TYPE_WIFI}. May be left as null when matchType is + * {@link NetworkTemplate.MATCH_WIFI_WILDCARD}. + * @param identSsid If networkType is {@code TYPE_WIFI}, this value must *NOT* be null. Provide + * one of {@code TEST_SSID*}. + */ + private fun matchOemManagedIdent( + networkType: Int, + matchType: Int, + subscriberId: String? = null, + templateSsid: String? = null, + identSsid: String? = null + ) { + val oemManagedStates = arrayOf(OEM_NONE, OEM_PAID, OEM_PRIVATE, OEM_PAID or OEM_PRIVATE) + val matchSubscriberIds = arrayOf(subscriberId) + + val templateOemYes = NetworkTemplate(matchType, subscriberId, matchSubscriberIds, + templateSsid, METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, + OEM_MANAGED_YES, SUBSCRIBER_ID_MATCH_RULE_EXACT) + val templateOemAll = NetworkTemplate(matchType, subscriberId, matchSubscriberIds, + templateSsid, METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, + OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT) + + for (identityOemManagedState in oemManagedStates) { + val ident = buildNetworkIdentity(mockContext, buildNetworkState(networkType, + subscriberId, identSsid, identityOemManagedState), /*defaultNetwork=*/false, + /*subType=*/0) + + // Create a template with each OEM managed type and match it against the NetworkIdentity + for (templateOemManagedState in oemManagedStates) { + val template = NetworkTemplate(matchType, subscriberId, matchSubscriberIds, + templateSsid, METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, + NETWORK_TYPE_ALL, templateOemManagedState, SUBSCRIBER_ID_MATCH_RULE_EXACT) + if (identityOemManagedState == templateOemManagedState) { + template.assertMatches(ident) + } else { + template.assertDoesNotMatch(ident) + } + } + // OEM_MANAGED_ALL ignores OEM state. + templateOemAll.assertMatches(ident) + if (identityOemManagedState == OEM_NONE) { + // OEM_MANAGED_YES matches everything except OEM_NONE. + templateOemYes.assertDoesNotMatch(ident) + } else { + templateOemYes.assertMatches(ident) + } + } + } + + @Test + fun testOemManagedMatchesIdent() { + matchOemManagedIdent(TYPE_MOBILE, MATCH_MOBILE, subscriberId = TEST_IMSI1) + matchOemManagedIdent(TYPE_MOBILE, MATCH_MOBILE_WILDCARD) + matchOemManagedIdent(TYPE_WIFI, MATCH_WIFI, templateSsid = TEST_SSID1, + identSsid = TEST_SSID1) + matchOemManagedIdent(TYPE_WIFI, MATCH_WIFI_WILDCARD, identSsid = TEST_SSID1) + } +} diff --git a/tests/unit/java/android/net/NetworkUtilsTest.java b/tests/unit/java/android/net/NetworkUtilsTest.java new file mode 100644 index 0000000000..7748288aeb --- /dev/null +++ b/tests/unit/java/android/net/NetworkUtilsTest.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2015 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; + +import static junit.framework.Assert.assertEquals; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.math.BigInteger; +import java.util.TreeSet; + +@RunWith(AndroidJUnit4.class) +@androidx.test.filters.SmallTest +public class NetworkUtilsTest { + @Test + public void testRoutedIPv4AddressCount() { + final TreeSet set = new TreeSet<>(IpPrefix.lengthComparator()); + // No routes routes to no addresses. + assertEquals(0, NetworkUtils.routedIPv4AddressCount(set)); + + set.add(new IpPrefix("0.0.0.0/0")); + assertEquals(1l << 32, NetworkUtils.routedIPv4AddressCount(set)); + + set.add(new IpPrefix("20.18.0.0/16")); + set.add(new IpPrefix("20.18.0.0/24")); + set.add(new IpPrefix("20.18.0.0/8")); + // There is a default route, still covers everything + assertEquals(1l << 32, NetworkUtils.routedIPv4AddressCount(set)); + + set.clear(); + set.add(new IpPrefix("20.18.0.0/24")); + set.add(new IpPrefix("20.18.0.0/8")); + // The 8-length includes the 24-length prefix + assertEquals(1l << 24, NetworkUtils.routedIPv4AddressCount(set)); + + set.add(new IpPrefix("10.10.10.126/25")); + // The 8-length does not include this 25-length prefix + assertEquals((1l << 24) + (1 << 7), NetworkUtils.routedIPv4AddressCount(set)); + + set.clear(); + set.add(new IpPrefix("1.2.3.4/32")); + set.add(new IpPrefix("1.2.3.4/32")); + set.add(new IpPrefix("1.2.3.4/32")); + set.add(new IpPrefix("1.2.3.4/32")); + assertEquals(1l, NetworkUtils.routedIPv4AddressCount(set)); + + set.add(new IpPrefix("1.2.3.5/32")); + set.add(new IpPrefix("1.2.3.6/32")); + + set.add(new IpPrefix("1.2.3.7/32")); + set.add(new IpPrefix("1.2.3.8/32")); + set.add(new IpPrefix("1.2.3.9/32")); + set.add(new IpPrefix("1.2.3.0/32")); + assertEquals(7l, NetworkUtils.routedIPv4AddressCount(set)); + + // 1.2.3.4/30 eats 1.2.3.{4-7}/32 + set.add(new IpPrefix("1.2.3.4/30")); + set.add(new IpPrefix("6.2.3.4/28")); + set.add(new IpPrefix("120.2.3.4/16")); + assertEquals(7l - 4 + 4 + 16 + 65536, NetworkUtils.routedIPv4AddressCount(set)); + } + + @Test + public void testRoutedIPv6AddressCount() { + final TreeSet set = new TreeSet<>(IpPrefix.lengthComparator()); + // No routes routes to no addresses. + assertEquals(BigInteger.ZERO, NetworkUtils.routedIPv6AddressCount(set)); + + set.add(new IpPrefix("::/0")); + assertEquals(BigInteger.ONE.shiftLeft(128), NetworkUtils.routedIPv6AddressCount(set)); + + set.add(new IpPrefix("1234:622a::18/64")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6adb/96")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6adb/8")); + // There is a default route, still covers everything + assertEquals(BigInteger.ONE.shiftLeft(128), NetworkUtils.routedIPv6AddressCount(set)); + + set.clear(); + set.add(new IpPrefix("add4:f00:80:f7:1111::6adb/96")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6adb/8")); + // The 8-length includes the 96-length prefix + assertEquals(BigInteger.ONE.shiftLeft(120), NetworkUtils.routedIPv6AddressCount(set)); + + set.add(new IpPrefix("10::26/64")); + // The 8-length does not include this 64-length prefix + assertEquals(BigInteger.ONE.shiftLeft(120).add(BigInteger.ONE.shiftLeft(64)), + NetworkUtils.routedIPv6AddressCount(set)); + + set.clear(); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad4/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad4/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad4/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad4/128")); + assertEquals(BigInteger.ONE, NetworkUtils.routedIPv6AddressCount(set)); + + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad5/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad6/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad7/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad8/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad9/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad0/128")); + assertEquals(BigInteger.valueOf(7), NetworkUtils.routedIPv6AddressCount(set)); + + // add4:f00:80:f7:1111::6ad4/126 eats add4:f00:8[:f7:1111::6ad{4-7}/128 + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad4/126")); + set.add(new IpPrefix("d00d:f00:80:f7:1111::6ade/124")); + set.add(new IpPrefix("f00b:a33::/112")); + assertEquals(BigInteger.valueOf(7l - 4 + 4 + 16 + 65536), + NetworkUtils.routedIPv6AddressCount(set)); + } +} diff --git a/tests/unit/java/android/net/QosSocketFilterTest.java b/tests/unit/java/android/net/QosSocketFilterTest.java new file mode 100644 index 0000000000..ad58960eaa --- /dev/null +++ b/tests/unit/java/android/net/QosSocketFilterTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2020 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; + +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.InetAddress; +import java.net.InetSocketAddress; + +@RunWith(AndroidJUnit4.class) +@androidx.test.filters.SmallTest +public class QosSocketFilterTest { + + @Test + public void testPortExactMatch() { + final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4"); + final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.4"); + assertTrue(QosSocketFilter.matchesLocalAddress( + new InetSocketAddress(addressA, 10), addressB, 10, 10)); + + } + + @Test + public void testPortLessThanStart() { + final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4"); + final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.4"); + assertFalse(QosSocketFilter.matchesLocalAddress( + new InetSocketAddress(addressA, 8), addressB, 10, 10)); + } + + @Test + public void testPortGreaterThanEnd() { + final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4"); + final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.4"); + assertFalse(QosSocketFilter.matchesLocalAddress( + new InetSocketAddress(addressA, 18), addressB, 10, 10)); + } + + @Test + public void testPortBetweenStartAndEnd() { + final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4"); + final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.4"); + assertTrue(QosSocketFilter.matchesLocalAddress( + new InetSocketAddress(addressA, 10), addressB, 8, 18)); + } + + @Test + public void testAddressesDontMatch() { + final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4"); + final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.5"); + assertFalse(QosSocketFilter.matchesLocalAddress( + new InetSocketAddress(addressA, 10), addressB, 10, 10)); + } +} + diff --git a/tests/unit/java/android/net/TelephonyNetworkSpecifierTest.java b/tests/unit/java/android/net/TelephonyNetworkSpecifierTest.java new file mode 100644 index 0000000000..6714bb1abb --- /dev/null +++ b/tests/unit/java/android/net/TelephonyNetworkSpecifierTest.java @@ -0,0 +1,113 @@ +/* + * 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; + +import static com.android.testutils.ParcelUtils.assertParcelSane; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.net.wifi.WifiNetworkSpecifier; +import android.telephony.SubscriptionManager; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; + +/** + * Unit test for {@link android.net.TelephonyNetworkSpecifier}. + */ +@SmallTest +public class TelephonyNetworkSpecifierTest { + private static final int TEST_SUBID = 5; + private static final String TEST_SSID = "Test123"; + + /** + * Validate that IllegalArgumentException will be thrown if build TelephonyNetworkSpecifier + * without calling {@link TelephonyNetworkSpecifier.Builder#setSubscriptionId(int)}. + */ + @Test + public void testBuilderBuildWithDefault() { + try { + new TelephonyNetworkSpecifier.Builder().build(); + } catch (IllegalArgumentException iae) { + // expected, test pass + } + } + + /** + * Validate that no exception will be thrown even if pass invalid subscription id to + * {@link TelephonyNetworkSpecifier.Builder#setSubscriptionId(int)}. + */ + @Test + public void testBuilderBuildWithInvalidSubId() { + TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier.Builder() + .setSubscriptionId(SubscriptionManager.INVALID_SUBSCRIPTION_ID) + .build(); + assertEquals(specifier.getSubscriptionId(), SubscriptionManager.INVALID_SUBSCRIPTION_ID); + } + + /** + * Validate the correctness of TelephonyNetworkSpecifier when provide valid subId. + */ + @Test + public void testBuilderBuildWithValidSubId() { + final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier.Builder() + .setSubscriptionId(TEST_SUBID) + .build(); + assertEquals(TEST_SUBID, specifier.getSubscriptionId()); + } + + /** + * Validate that parcel marshalling/unmarshalling works. + */ + @Test + public void testParcel() { + TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier.Builder() + .setSubscriptionId(TEST_SUBID) + .build(); + assertParcelSane(specifier, 1 /* fieldCount */); + } + + /** + * Validate the behavior of method canBeSatisfiedBy(). + */ + @Test + public void testCanBeSatisfiedBy() { + final TelephonyNetworkSpecifier tns1 = new TelephonyNetworkSpecifier.Builder() + .setSubscriptionId(TEST_SUBID) + .build(); + final TelephonyNetworkSpecifier tns2 = new TelephonyNetworkSpecifier.Builder() + .setSubscriptionId(TEST_SUBID) + .build(); + final WifiNetworkSpecifier wns = new WifiNetworkSpecifier.Builder() + .setSsid(TEST_SSID) + .build(); + final MatchAllNetworkSpecifier mans = new MatchAllNetworkSpecifier(); + + // Test equality + assertEquals(tns1, tns2); + assertTrue(tns1.canBeSatisfiedBy(tns1)); + assertTrue(tns1.canBeSatisfiedBy(tns2)); + + // Test other edge cases. + assertFalse(tns1.canBeSatisfiedBy(null)); + assertFalse(tns1.canBeSatisfiedBy(wns)); + assertTrue(tns1.canBeSatisfiedBy(mans)); + } +} diff --git a/tests/unit/java/android/net/VpnManagerTest.java b/tests/unit/java/android/net/VpnManagerTest.java new file mode 100644 index 0000000000..3135062138 --- /dev/null +++ b/tests/unit/java/android/net/VpnManagerTest.java @@ -0,0 +1,138 @@ +/* + * 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.Intent; +import android.test.mock.MockContext; +import android.util.SparseArray; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.net.VpnProfile; +import com.android.internal.util.MessageUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link VpnManager}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class VpnManagerTest { + private static final String PKG_NAME = "fooPackage"; + + private static final String SESSION_NAME_STRING = "testSession"; + private static final String SERVER_ADDR_STRING = "1.2.3.4"; + private static final String IDENTITY_STRING = "Identity"; + private static final byte[] PSK_BYTES = "preSharedKey".getBytes(); + + private IVpnManager mMockService; + private VpnManager mVpnManager; + private final MockContext mMockContext = + new MockContext() { + @Override + public String getOpPackageName() { + return PKG_NAME; + } + }; + + @Before + public void setUp() throws Exception { + mMockService = mock(IVpnManager.class); + mVpnManager = new VpnManager(mMockContext, mMockService); + } + + @Test + public void testProvisionVpnProfilePreconsented() throws Exception { + final PlatformVpnProfile profile = getPlatformVpnProfile(); + when(mMockService.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME))) + .thenReturn(true); + + // Expect there to be no intent returned, as consent has already been granted. + assertNull(mVpnManager.provisionVpnProfile(profile)); + verify(mMockService).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME)); + } + + @Test + public void testProvisionVpnProfileNeedsConsent() throws Exception { + final PlatformVpnProfile profile = getPlatformVpnProfile(); + when(mMockService.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME))) + .thenReturn(false); + + // Expect intent to be returned, as consent has not already been granted. + final Intent intent = mVpnManager.provisionVpnProfile(profile); + assertNotNull(intent); + + final ComponentName expectedComponentName = + ComponentName.unflattenFromString( + "com.android.vpndialogs/com.android.vpndialogs.PlatformVpnConfirmDialog"); + assertEquals(expectedComponentName, intent.getComponent()); + verify(mMockService).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME)); + } + + @Test + public void testDeleteProvisionedVpnProfile() throws Exception { + mVpnManager.deleteProvisionedVpnProfile(); + verify(mMockService).deleteVpnProfile(eq(PKG_NAME)); + } + + @Test + public void testStartProvisionedVpnProfile() throws Exception { + mVpnManager.startProvisionedVpnProfile(); + verify(mMockService).startVpnProfile(eq(PKG_NAME)); + } + + @Test + public void testStopProvisionedVpnProfile() throws Exception { + mVpnManager.stopProvisionedVpnProfile(); + verify(mMockService).stopVpnProfile(eq(PKG_NAME)); + } + + private Ikev2VpnProfile getPlatformVpnProfile() throws Exception { + return new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING) + .setBypassable(true) + .setMaxMtu(1300) + .setMetered(true) + .setAuthPsk(PSK_BYTES) + .build(); + } + + @Test + public void testVpnTypesEqual() throws Exception { + SparseArray vmVpnTypes = MessageUtils.findMessageNames( + new Class[] { VpnManager.class }, new String[]{ "TYPE_VPN_" }); + SparseArray nativeVpnType = MessageUtils.findMessageNames( + new Class[] { NativeVpnType.class }, new String[]{ "" }); + + // TYPE_VPN_NONE = -1 is only defined in VpnManager. + assertEquals(vmVpnTypes.size() - 1, nativeVpnType.size()); + for (int i = VpnManager.TYPE_VPN_SERVICE; i < vmVpnTypes.size(); i++) { + assertEquals(vmVpnTypes.get(i), "TYPE_VPN_" + nativeVpnType.get(i)); + } + } +} diff --git a/tests/unit/java/android/net/VpnTransportInfoTest.java b/tests/unit/java/android/net/VpnTransportInfoTest.java new file mode 100644 index 0000000000..ccaa5cf7e9 --- /dev/null +++ b/tests/unit/java/android/net/VpnTransportInfoTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2021 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; + +import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS; +import static android.net.NetworkCapabilities.REDACT_NONE; + +import static com.android.testutils.ParcelUtils.assertParcelSane; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class VpnTransportInfoTest { + + @Test + public void testParceling() { + VpnTransportInfo v = new VpnTransportInfo(VpnManager.TYPE_VPN_PLATFORM, "12345"); + assertParcelSane(v, 2 /* fieldCount */); + } + + @Test + public void testEqualsAndHashCode() { + String session1 = "12345"; + String session2 = "6789"; + VpnTransportInfo v11 = new VpnTransportInfo(VpnManager.TYPE_VPN_PLATFORM, session1); + VpnTransportInfo v12 = new VpnTransportInfo(VpnManager.TYPE_VPN_SERVICE, session1); + VpnTransportInfo v13 = new VpnTransportInfo(VpnManager.TYPE_VPN_PLATFORM, session1); + VpnTransportInfo v14 = new VpnTransportInfo(VpnManager.TYPE_VPN_LEGACY, session1); + VpnTransportInfo v15 = new VpnTransportInfo(VpnManager.TYPE_VPN_OEM, session1); + VpnTransportInfo v21 = new VpnTransportInfo(VpnManager.TYPE_VPN_LEGACY, session2); + + VpnTransportInfo v31 = v11.makeCopy(REDACT_FOR_NETWORK_SETTINGS); + VpnTransportInfo v32 = v13.makeCopy(REDACT_FOR_NETWORK_SETTINGS); + + assertNotEquals(v11, v12); + assertNotEquals(v13, v14); + assertNotEquals(v14, v15); + assertNotEquals(v14, v21); + + assertEquals(v11, v13); + assertEquals(v31, v32); + assertEquals(v11.hashCode(), v13.hashCode()); + assertEquals(REDACT_FOR_NETWORK_SETTINGS, v32.getApplicableRedactions()); + assertEquals(session1, v15.makeCopy(REDACT_NONE).getSessionId()); + } +} diff --git a/tests/unit/java/android/net/ipmemorystore/ParcelableTests.java b/tests/unit/java/android/net/ipmemorystore/ParcelableTests.java new file mode 100644 index 0000000000..603c875195 --- /dev/null +++ b/tests/unit/java/android/net/ipmemorystore/ParcelableTests.java @@ -0,0 +1,142 @@ +/* + * 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.ipmemorystore; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.net.networkstack.aidl.quirks.IPv6ProvisioningLossQuirk; +import android.net.networkstack.aidl.quirks.IPv6ProvisioningLossQuirkParcelable; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.lang.reflect.Modifier; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.util.Arrays; +import java.util.Collections; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ParcelableTests { + @Test + public void testNetworkAttributesParceling() throws Exception { + final NetworkAttributes.Builder builder = new NetworkAttributes.Builder(); + NetworkAttributes in = builder.build(); + assertEquals(in, new NetworkAttributes(parcelingRoundTrip(in.toParcelable()))); + + builder.setAssignedV4Address((Inet4Address) Inet4Address.getByName("1.2.3.4")); + // lease will expire in two hours + builder.setAssignedV4AddressExpiry(System.currentTimeMillis() + 7_200_000); + // cluster stays null this time around + builder.setDnsAddresses(Collections.emptyList()); + builder.setMtu(18); + in = builder.build(); + assertEquals(in, new NetworkAttributes(parcelingRoundTrip(in.toParcelable()))); + + builder.setAssignedV4Address((Inet4Address) Inet4Address.getByName("6.7.8.9")); + builder.setAssignedV4AddressExpiry(System.currentTimeMillis() + 3_600_000); + builder.setCluster("groupHint"); + builder.setDnsAddresses(Arrays.asList( + InetAddress.getByName("ACA1:652B:0911:DE8F:1200:115E:913B:AA2A"), + InetAddress.getByName("6.7.8.9"))); + builder.setMtu(1_000_000); + in = builder.build(); + assertEquals(in, new NetworkAttributes(parcelingRoundTrip(in.toParcelable()))); + + builder.setMtu(null); + in = builder.build(); + assertEquals(in, new NetworkAttributes(parcelingRoundTrip(in.toParcelable()))); + + // Verify that this test does not miss any new field added later. + // If any field is added to NetworkAttributes it must be tested here for parceling + // roundtrip. + assertEquals(6, Arrays.stream(NetworkAttributes.class.getDeclaredFields()) + .filter(f -> !Modifier.isStatic(f.getModifiers())).count()); + } + + @Test + public void testPrivateDataParceling() throws Exception { + final Blob in = new Blob(); + in.data = new byte[] {89, 111, 108, 111}; + final Blob out = parcelingRoundTrip(in); + // Object.equals on byte[] tests the references + assertEquals(in.data.length, out.data.length); + assertTrue(Arrays.equals(in.data, out.data)); + } + + @Test + public void testSameL3NetworkResponseParceling() throws Exception { + final SameL3NetworkResponseParcelable parcelable = new SameL3NetworkResponseParcelable(); + parcelable.l2Key1 = "key 1"; + parcelable.l2Key2 = "key 2"; + parcelable.confidence = 0.43f; + + final SameL3NetworkResponse in = new SameL3NetworkResponse(parcelable); + assertEquals("key 1", in.l2Key1); + assertEquals("key 2", in.l2Key2); + assertEquals(0.43f, in.confidence, 0.01f /* delta */); + + final SameL3NetworkResponse out = + new SameL3NetworkResponse(parcelingRoundTrip(in.toParcelable())); + + assertEquals(in, out); + assertEquals(in.l2Key1, out.l2Key1); + assertEquals(in.l2Key2, out.l2Key2); + assertEquals(in.confidence, out.confidence, 0.01f /* delta */); + } + + @Test + public void testIPv6ProvisioningLossQuirkParceling() throws Exception { + final NetworkAttributes.Builder builder = new NetworkAttributes.Builder(); + final IPv6ProvisioningLossQuirkParcelable parcelable = + new IPv6ProvisioningLossQuirkParcelable(); + final long expiry = System.currentTimeMillis() + 7_200_000; + + parcelable.detectionCount = 3; + parcelable.quirkExpiry = expiry; // quirk info will expire in two hours + builder.setIpv6ProvLossQuirk(IPv6ProvisioningLossQuirk.fromStableParcelable(parcelable)); + final NetworkAttributes in = builder.build(); + + final NetworkAttributes out = new NetworkAttributes(parcelingRoundTrip(in.toParcelable())); + assertEquals(out.ipv6ProvisioningLossQuirk, in.ipv6ProvisioningLossQuirk); + } + + private T parcelingRoundTrip(final T in) throws Exception { + final Parcel p = Parcel.obtain(); + in.writeToParcel(p, /* flags */ 0); + p.setDataPosition(0); + final byte[] marshalledData = p.marshall(); + p.recycle(); + + final Parcel q = Parcel.obtain(); + q.unmarshall(marshalledData, 0, marshalledData.length); + q.setDataPosition(0); + + final Parcelable.Creator creator = (Parcelable.Creator) + in.getClass().getField("CREATOR").get(null); // static object, so null receiver + final T unmarshalled = (T) creator.createFromParcel(q); + q.recycle(); + return unmarshalled; + } +} diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java new file mode 100644 index 0000000000..b0a9b8a553 --- /dev/null +++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java @@ -0,0 +1,388 @@ +/* + * 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 android.net.nsd; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.AsyncChannel; +import com.android.testutils.HandlerUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NsdManagerTest { + + static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD; + + @Mock Context mContext; + @Mock INsdManager mService; + MockServiceHandler mServiceHandler; + + NsdManager mManager; + + long mTimeoutMs = 200; // non-final so that tests can adjust the value. + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mServiceHandler = spy(MockServiceHandler.create(mContext)); + when(mService.getMessenger()).thenReturn(new Messenger(mServiceHandler)); + + mManager = makeManager(); + } + + @After + public void tearDown() throws Exception { + HandlerUtils.waitForIdle(mServiceHandler, mTimeoutMs); + mServiceHandler.chan.disconnect(); + mServiceHandler.stop(); + if (mManager != null) { + mManager.disconnect(); + } + } + + @Test + public void testResolveService() { + NsdManager manager = mManager; + + NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type"); + NsdServiceInfo reply = new NsdServiceInfo("resolved_name", "resolved_type"); + NsdManager.ResolveListener listener = mock(NsdManager.ResolveListener.class); + + manager.resolveService(request, listener); + int key1 = verifyRequest(NsdManager.RESOLVE_SERVICE); + int err = 33; + sendResponse(NsdManager.RESOLVE_SERVICE_FAILED, err, key1, null); + verify(listener, timeout(mTimeoutMs).times(1)).onResolveFailed(request, err); + + manager.resolveService(request, listener); + int key2 = verifyRequest(NsdManager.RESOLVE_SERVICE); + sendResponse(NsdManager.RESOLVE_SERVICE_SUCCEEDED, 0, key2, reply); + verify(listener, timeout(mTimeoutMs).times(1)).onServiceResolved(reply); + } + + @Test + public void testParallelResolveService() { + NsdManager manager = mManager; + + NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type"); + NsdServiceInfo reply = new NsdServiceInfo("resolved_name", "resolved_type"); + + NsdManager.ResolveListener listener1 = mock(NsdManager.ResolveListener.class); + NsdManager.ResolveListener listener2 = mock(NsdManager.ResolveListener.class); + + manager.resolveService(request, listener1); + int key1 = verifyRequest(NsdManager.RESOLVE_SERVICE); + + manager.resolveService(request, listener2); + int key2 = verifyRequest(NsdManager.RESOLVE_SERVICE); + + sendResponse(NsdManager.RESOLVE_SERVICE_SUCCEEDED, 0, key2, reply); + sendResponse(NsdManager.RESOLVE_SERVICE_SUCCEEDED, 0, key1, reply); + + verify(listener1, timeout(mTimeoutMs).times(1)).onServiceResolved(reply); + verify(listener2, timeout(mTimeoutMs).times(1)).onServiceResolved(reply); + } + + @Test + public void testRegisterService() { + NsdManager manager = mManager; + + NsdServiceInfo request1 = new NsdServiceInfo("a_name", "a_type"); + NsdServiceInfo request2 = new NsdServiceInfo("another_name", "another_type"); + request1.setPort(2201); + request2.setPort(2202); + NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class); + NsdManager.RegistrationListener listener2 = mock(NsdManager.RegistrationListener.class); + + // Register two services + manager.registerService(request1, PROTOCOL, listener1); + int key1 = verifyRequest(NsdManager.REGISTER_SERVICE); + + manager.registerService(request2, PROTOCOL, listener2); + int key2 = verifyRequest(NsdManager.REGISTER_SERVICE); + + // First reques fails, second request succeeds + sendResponse(NsdManager.REGISTER_SERVICE_SUCCEEDED, 0, key2, request2); + verify(listener2, timeout(mTimeoutMs).times(1)).onServiceRegistered(request2); + + int err = 1; + sendResponse(NsdManager.REGISTER_SERVICE_FAILED, err, key1, request1); + verify(listener1, timeout(mTimeoutMs).times(1)).onRegistrationFailed(request1, err); + + // Client retries first request, it succeeds + manager.registerService(request1, PROTOCOL, listener1); + int key3 = verifyRequest(NsdManager.REGISTER_SERVICE); + + sendResponse(NsdManager.REGISTER_SERVICE_SUCCEEDED, 0, key3, request1); + verify(listener1, timeout(mTimeoutMs).times(1)).onServiceRegistered(request1); + + // First request is unregistered, it succeeds + manager.unregisterService(listener1); + int key3again = verifyRequest(NsdManager.UNREGISTER_SERVICE); + assertEquals(key3, key3again); + + sendResponse(NsdManager.UNREGISTER_SERVICE_SUCCEEDED, 0, key3again, null); + verify(listener1, timeout(mTimeoutMs).times(1)).onServiceUnregistered(request1); + + // Second request is unregistered, it fails + manager.unregisterService(listener2); + int key2again = verifyRequest(NsdManager.UNREGISTER_SERVICE); + assertEquals(key2, key2again); + + sendResponse(NsdManager.UNREGISTER_SERVICE_FAILED, err, key2again, null); + verify(listener2, timeout(mTimeoutMs).times(1)).onUnregistrationFailed(request2, err); + + // TODO: do not unregister listener until service is unregistered + // Client retries unregistration of second request, it succeeds + //manager.unregisterService(listener2); + //int key2yetAgain = verifyRequest(NsdManager.UNREGISTER_SERVICE); + //assertEquals(key2, key2yetAgain); + + //sendResponse(NsdManager.UNREGISTER_SERVICE_SUCCEEDED, 0, key2yetAgain, null); + //verify(listener2, timeout(mTimeoutMs).times(1)).onServiceUnregistered(request2); + } + + @Test + public void testDiscoverService() { + NsdManager manager = mManager; + + NsdServiceInfo reply1 = new NsdServiceInfo("a_name", "a_type"); + NsdServiceInfo reply2 = new NsdServiceInfo("another_name", "a_type"); + NsdServiceInfo reply3 = new NsdServiceInfo("a_third_name", "a_type"); + + NsdManager.DiscoveryListener listener = mock(NsdManager.DiscoveryListener.class); + + // Client registers for discovery, request fails + manager.discoverServices("a_type", PROTOCOL, listener); + int key1 = verifyRequest(NsdManager.DISCOVER_SERVICES); + + int err = 1; + sendResponse(NsdManager.DISCOVER_SERVICES_FAILED, err, key1, null); + verify(listener, timeout(mTimeoutMs).times(1)).onStartDiscoveryFailed("a_type", err); + + // Client retries, request succeeds + manager.discoverServices("a_type", PROTOCOL, listener); + int key2 = verifyRequest(NsdManager.DISCOVER_SERVICES); + + sendResponse(NsdManager.DISCOVER_SERVICES_STARTED, 0, key2, reply1); + verify(listener, timeout(mTimeoutMs).times(1)).onDiscoveryStarted("a_type"); + + + // mdns notifies about services + sendResponse(NsdManager.SERVICE_FOUND, 0, key2, reply1); + verify(listener, timeout(mTimeoutMs).times(1)).onServiceFound(reply1); + + sendResponse(NsdManager.SERVICE_FOUND, 0, key2, reply2); + verify(listener, timeout(mTimeoutMs).times(1)).onServiceFound(reply2); + + sendResponse(NsdManager.SERVICE_LOST, 0, key2, reply2); + verify(listener, timeout(mTimeoutMs).times(1)).onServiceLost(reply2); + + + // Client unregisters its listener + manager.stopServiceDiscovery(listener); + int key2again = verifyRequest(NsdManager.STOP_DISCOVERY); + assertEquals(key2, key2again); + + // TODO: unregister listener immediately and stop notifying it about services + // Notifications are still passed to the client's listener + sendResponse(NsdManager.SERVICE_LOST, 0, key2, reply1); + verify(listener, timeout(mTimeoutMs).times(1)).onServiceLost(reply1); + + // Client is notified of complete unregistration + sendResponse(NsdManager.STOP_DISCOVERY_SUCCEEDED, 0, key2again, "a_type"); + verify(listener, timeout(mTimeoutMs).times(1)).onDiscoveryStopped("a_type"); + + // Notifications are not passed to the client anymore + sendResponse(NsdManager.SERVICE_FOUND, 0, key2, reply3); + verify(listener, timeout(mTimeoutMs).times(0)).onServiceLost(reply3); + + + // Client registers for service discovery + reset(listener); + manager.discoverServices("a_type", PROTOCOL, listener); + int key3 = verifyRequest(NsdManager.DISCOVER_SERVICES); + + sendResponse(NsdManager.DISCOVER_SERVICES_STARTED, 0, key3, reply1); + verify(listener, timeout(mTimeoutMs).times(1)).onDiscoveryStarted("a_type"); + + // Client unregisters immediately, it fails + manager.stopServiceDiscovery(listener); + int key3again = verifyRequest(NsdManager.STOP_DISCOVERY); + assertEquals(key3, key3again); + + err = 2; + sendResponse(NsdManager.STOP_DISCOVERY_FAILED, err, key3again, "a_type"); + verify(listener, timeout(mTimeoutMs).times(1)).onStopDiscoveryFailed("a_type", err); + + // New notifications are not passed to the client anymore + sendResponse(NsdManager.SERVICE_FOUND, 0, key3, reply1); + verify(listener, timeout(mTimeoutMs).times(0)).onServiceFound(reply1); + } + + @Test + public void testInvalidCalls() { + NsdManager manager = mManager; + + NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class); + NsdManager.DiscoveryListener listener2 = mock(NsdManager.DiscoveryListener.class); + NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class); + + NsdServiceInfo invalidService = new NsdServiceInfo(null, null); + NsdServiceInfo validService = new NsdServiceInfo("a_name", "a_type"); + validService.setPort(2222); + + // Service registration + // - invalid arguments + mustFail(() -> { manager.unregisterService(null); }); + mustFail(() -> { manager.registerService(null, -1, null); }); + mustFail(() -> { manager.registerService(null, PROTOCOL, listener1); }); + mustFail(() -> { manager.registerService(invalidService, PROTOCOL, listener1); }); + mustFail(() -> { manager.registerService(validService, -1, listener1); }); + mustFail(() -> { manager.registerService(validService, PROTOCOL, null); }); + manager.registerService(validService, PROTOCOL, listener1); + // - listener already registered + mustFail(() -> { manager.registerService(validService, PROTOCOL, listener1); }); + manager.unregisterService(listener1); + // TODO: make listener immediately reusable + //mustFail(() -> { manager.unregisterService(listener1); }); + //manager.registerService(validService, PROTOCOL, listener1); + + // Discover service + // - invalid arguments + mustFail(() -> { manager.stopServiceDiscovery(null); }); + mustFail(() -> { manager.discoverServices(null, -1, null); }); + mustFail(() -> { manager.discoverServices(null, PROTOCOL, listener2); }); + mustFail(() -> { manager.discoverServices("a_service", -1, listener2); }); + mustFail(() -> { manager.discoverServices("a_service", PROTOCOL, null); }); + manager.discoverServices("a_service", PROTOCOL, listener2); + // - listener already registered + mustFail(() -> { manager.discoverServices("another_service", PROTOCOL, listener2); }); + manager.stopServiceDiscovery(listener2); + // TODO: make listener immediately reusable + //mustFail(() -> { manager.stopServiceDiscovery(listener2); }); + //manager.discoverServices("another_service", PROTOCOL, listener2); + + // Resolver service + // - invalid arguments + mustFail(() -> { manager.resolveService(null, null); }); + mustFail(() -> { manager.resolveService(null, listener3); }); + mustFail(() -> { manager.resolveService(invalidService, listener3); }); + mustFail(() -> { manager.resolveService(validService, null); }); + manager.resolveService(validService, listener3); + // - listener already registered:w + mustFail(() -> { manager.resolveService(validService, listener3); }); + } + + public void mustFail(Runnable fn) { + try { + fn.run(); + fail(); + } catch (Exception expected) { + } + } + + NsdManager makeManager() { + NsdManager manager = new NsdManager(mContext, mService); + // Acknowledge first two messages connecting the AsyncChannel. + verify(mServiceHandler, timeout(mTimeoutMs).times(2)).handleMessage(any()); + reset(mServiceHandler); + assertNotNull(mServiceHandler.chan); + return manager; + } + + int verifyRequest(int expectedMessageType) { + HandlerUtils.waitForIdle(mServiceHandler, mTimeoutMs); + verify(mServiceHandler, timeout(mTimeoutMs)).handleMessage(any()); + reset(mServiceHandler); + Message received = mServiceHandler.getLastMessage(); + assertEquals(NsdManager.nameOf(expectedMessageType), NsdManager.nameOf(received.what)); + return received.arg2; + } + + void sendResponse(int replyType, int arg, int key, Object obj) { + mServiceHandler.chan.sendMessage(replyType, arg, key, obj); + } + + // Implements the server side of AsyncChannel connection protocol + public static class MockServiceHandler extends Handler { + public final Context context; + public AsyncChannel chan; + public Message lastMessage; + + MockServiceHandler(Looper l, Context c) { + super(l); + context = c; + } + + synchronized Message getLastMessage() { + return lastMessage; + } + + synchronized void setLastMessage(Message msg) { + lastMessage = obtainMessage(); + lastMessage.copyFrom(msg); + } + + @Override + public void handleMessage(Message msg) { + setLastMessage(msg); + if (msg.what == AsyncChannel.CMD_CHANNEL_FULL_CONNECTION) { + chan = new AsyncChannel(); + chan.connect(context, this, msg.replyTo); + chan.sendMessage(AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED); + } + } + + void stop() { + getLooper().quitSafely(); + } + + static MockServiceHandler create(Context context) { + HandlerThread t = new HandlerThread("mock-service-handler"); + t.start(); + return new MockServiceHandler(t.getLooper(), context); + } + } +} diff --git a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java new file mode 100644 index 0000000000..94dfc7515c --- /dev/null +++ b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2014 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.nsd; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.StrictMode; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Map; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NsdServiceInfoTest { + + public final static InetAddress LOCALHOST; + static { + // Because test. + StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build(); + StrictMode.setThreadPolicy(policy); + + InetAddress _host = null; + try { + _host = InetAddress.getLocalHost(); + } catch (UnknownHostException e) { } + LOCALHOST = _host; + } + + @Test + public void testLimits() throws Exception { + NsdServiceInfo info = new NsdServiceInfo(); + + // Non-ASCII keys. + boolean exceptionThrown = false; + try { + info.setAttribute("猫", "meow"); + } catch (IllegalArgumentException e) { + exceptionThrown = true; + } + assertTrue(exceptionThrown); + assertEmptyServiceInfo(info); + + // ASCII keys with '=' character. + exceptionThrown = false; + try { + info.setAttribute("kitten=", "meow"); + } catch (IllegalArgumentException e) { + exceptionThrown = true; + } + assertTrue(exceptionThrown); + assertEmptyServiceInfo(info); + + // Single key + value length too long. + exceptionThrown = false; + try { + String longValue = "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" + + "oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" + + "oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" + + "ooooooooooooooooooooooooooooong"; // 248 characters. + info.setAttribute("longcat", longValue); // Key + value == 255 characters. + } catch (IllegalArgumentException e) { + exceptionThrown = true; + } + assertTrue(exceptionThrown); + assertEmptyServiceInfo(info); + + // Total TXT record length too long. + exceptionThrown = false; + int recordsAdded = 0; + try { + for (int i = 100; i < 300; ++i) { + // 6 char key + 5 char value + 2 bytes overhead = 13 byte record length. + String key = String.format("key%d", i); + info.setAttribute(key, "12345"); + recordsAdded++; + } + } catch (IllegalArgumentException e) { + exceptionThrown = true; + } + assertTrue(exceptionThrown); + assertTrue(100 == recordsAdded); + assertTrue(info.getTxtRecord().length == 1300); + } + + @Test + public void testParcel() throws Exception { + NsdServiceInfo emptyInfo = new NsdServiceInfo(); + checkParcelable(emptyInfo); + + NsdServiceInfo fullInfo = new NsdServiceInfo(); + fullInfo.setServiceName("kitten"); + fullInfo.setServiceType("_kitten._tcp"); + fullInfo.setPort(4242); + fullInfo.setHost(LOCALHOST); + checkParcelable(fullInfo); + + NsdServiceInfo noHostInfo = new NsdServiceInfo(); + noHostInfo.setServiceName("kitten"); + noHostInfo.setServiceType("_kitten._tcp"); + noHostInfo.setPort(4242); + checkParcelable(noHostInfo); + + NsdServiceInfo attributedInfo = new NsdServiceInfo(); + attributedInfo.setServiceName("kitten"); + attributedInfo.setServiceType("_kitten._tcp"); + attributedInfo.setPort(4242); + attributedInfo.setHost(LOCALHOST); + attributedInfo.setAttribute("color", "pink"); + attributedInfo.setAttribute("sound", (new String("にゃあ")).getBytes("UTF-8")); + attributedInfo.setAttribute("adorable", (String) null); + attributedInfo.setAttribute("sticky", "yes"); + attributedInfo.setAttribute("siblings", new byte[] {}); + attributedInfo.setAttribute("edge cases", new byte[] {0, -1, 127, -128}); + attributedInfo.removeAttribute("sticky"); + checkParcelable(attributedInfo); + + // Sanity check that we actually wrote attributes to attributedInfo. + assertTrue(attributedInfo.getAttributes().keySet().contains("adorable")); + String sound = new String(attributedInfo.getAttributes().get("sound"), "UTF-8"); + assertTrue(sound.equals("にゃあ")); + byte[] edgeCases = attributedInfo.getAttributes().get("edge cases"); + assertTrue(Arrays.equals(edgeCases, new byte[] {0, -1, 127, -128})); + assertFalse(attributedInfo.getAttributes().keySet().contains("sticky")); + } + + public void checkParcelable(NsdServiceInfo original) { + // Write to parcel. + Parcel p = Parcel.obtain(); + Bundle writer = new Bundle(); + writer.putParcelable("test_info", original); + writer.writeToParcel(p, 0); + + // Extract from parcel. + p.setDataPosition(0); + Bundle reader = p.readBundle(); + reader.setClassLoader(NsdServiceInfo.class.getClassLoader()); + NsdServiceInfo result = reader.getParcelable("test_info"); + + // Assert equality of base fields. + assertEquals(original.getServiceName(), result.getServiceName()); + assertEquals(original.getServiceType(), result.getServiceType()); + assertEquals(original.getHost(), result.getHost()); + assertTrue(original.getPort() == result.getPort()); + + // Assert equality of attribute map. + Map originalMap = original.getAttributes(); + Map resultMap = result.getAttributes(); + assertEquals(originalMap.keySet(), resultMap.keySet()); + for (String key : originalMap.keySet()) { + assertTrue(Arrays.equals(originalMap.get(key), resultMap.get(key))); + } + } + + public void assertEmptyServiceInfo(NsdServiceInfo shouldBeEmpty) { + byte[] txtRecord = shouldBeEmpty.getTxtRecord(); + if (txtRecord == null || txtRecord.length == 0) { + return; + } + fail("NsdServiceInfo.getTxtRecord did not return null but " + Arrays.toString(txtRecord)); + } +} diff --git a/tests/unit/java/android/net/util/DnsUtilsTest.java b/tests/unit/java/android/net/util/DnsUtilsTest.java new file mode 100644 index 0000000000..b626db8d89 --- /dev/null +++ b/tests/unit/java/android/net/util/DnsUtilsTest.java @@ -0,0 +1,216 @@ +/* + * 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.util; + +import static android.net.util.DnsUtils.IPV6_ADDR_SCOPE_GLOBAL; +import static android.net.util.DnsUtils.IPV6_ADDR_SCOPE_LINKLOCAL; +import static android.net.util.DnsUtils.IPV6_ADDR_SCOPE_SITELOCAL; + +import static org.junit.Assert.assertEquals; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.InetAddresses; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.InetAddress; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class DnsUtilsTest { + private InetAddress stringToAddress(@NonNull String addr) { + return InetAddresses.parseNumericAddress(addr); + } + + private DnsUtils.SortableAddress makeSortableAddress(@NonNull String addr) { + return makeSortableAddress(addr, null); + } + + private DnsUtils.SortableAddress makeSortableAddress(@NonNull String addr, + @Nullable String srcAddr) { + return new DnsUtils.SortableAddress(stringToAddress(addr), + srcAddr != null ? stringToAddress(srcAddr) : null); + } + + @Test + public void testRfc6724Comparator() { + final List test = Arrays.asList( + // Ipv4 + makeSortableAddress("216.58.200.36", "192.168.1.1"), + // global with different scope src + makeSortableAddress("2404:6800:4008:801::2004", "fe80::1111:2222"), + // global without src addr + makeSortableAddress("2404:6800:cafe:801::1"), + // loop back + makeSortableAddress("::1", "::1"), + // link local + makeSortableAddress("fe80::c46f:1cff:fe04:39b4", "fe80::1"), + // teredo tunneling + makeSortableAddress("2001::47c1", "2001::2"), + // 6bone without src addr + makeSortableAddress("3ffe::1234:5678"), + // IPv4-compatible + makeSortableAddress("::216.58.200.36", "::216.58.200.9"), + // 6bone + makeSortableAddress("3ffe::1234:5678", "3ffe::1234:1"), + // IPv4-mapped IPv6 + makeSortableAddress("::ffff:192.168.95.7", "::ffff:192.168.95.1")); + + final List expected = Arrays.asList( + stringToAddress("::1"), // loop back + stringToAddress("fe80::c46f:1cff:fe04:39b4"), // link local + stringToAddress("216.58.200.36"), // Ipv4 + stringToAddress("::ffff:192.168.95.7"), // IPv4-mapped IPv6 + stringToAddress("2001::47c1"), // teredo tunneling + stringToAddress("::216.58.200.36"), // IPv4-compatible + stringToAddress("3ffe::1234:5678"), // 6bone + stringToAddress("2404:6800:4008:801::2004"), // global with different scope src + stringToAddress("2404:6800:cafe:801::1"), // global without src addr + stringToAddress("3ffe::1234:5678")); // 6bone without src addr + + Collections.sort(test, new DnsUtils.Rfc6724Comparator()); + + for (int i = 0; i < test.size(); ++i) { + assertEquals(test.get(i).address, expected.get(i)); + } + + // TODO: add more combinations + } + + @Test + public void testV4SortableAddress() { + // Test V4 address + DnsUtils.SortableAddress test = makeSortableAddress("216.58.200.36"); + assertEquals(test.hasSrcAddr, 0); + assertEquals(test.prefixMatchLen, 0); + assertEquals(test.address, stringToAddress("216.58.200.36")); + assertEquals(test.labelMatch, 0); + assertEquals(test.scopeMatch, 0); + assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL); + assertEquals(test.label, 4); + assertEquals(test.precedence, 35); + + // Test V4 loopback address with the same source address + test = makeSortableAddress("127.1.2.3", "127.1.2.3"); + assertEquals(test.hasSrcAddr, 1); + assertEquals(test.prefixMatchLen, 0); + assertEquals(test.address, stringToAddress("127.1.2.3")); + assertEquals(test.labelMatch, 1); + assertEquals(test.scopeMatch, 1); + assertEquals(test.scope, IPV6_ADDR_SCOPE_LINKLOCAL); + assertEquals(test.label, 4); + assertEquals(test.precedence, 35); + } + + @Test + public void testV6SortableAddress() { + // Test global address + DnsUtils.SortableAddress test = makeSortableAddress("2404:6800:4008:801::2004"); + assertEquals(test.address, stringToAddress("2404:6800:4008:801::2004")); + assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL); + assertEquals(test.label, 1); + assertEquals(test.precedence, 40); + + // Test global address with global source address + test = makeSortableAddress("2404:6800:4008:801::2004", + "2401:fa00:fc:fd00:6d6c:7199:b8e7:41d6"); + assertEquals(test.address, stringToAddress("2404:6800:4008:801::2004")); + assertEquals(test.hasSrcAddr, 1); + assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL); + assertEquals(test.labelMatch, 1); + assertEquals(test.scopeMatch, 1); + assertEquals(test.label, 1); + assertEquals(test.precedence, 40); + assertEquals(test.prefixMatchLen, 13); + + // Test global address with linklocal source address + test = makeSortableAddress("2404:6800:4008:801::2004", "fe80::c46f:1cff:fe04:39b4"); + assertEquals(test.hasSrcAddr, 1); + assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL); + assertEquals(test.labelMatch, 1); + assertEquals(test.scopeMatch, 0); + assertEquals(test.label, 1); + assertEquals(test.precedence, 40); + assertEquals(test.prefixMatchLen, 0); + + // Test loopback address with the same source address + test = makeSortableAddress("::1", "::1"); + assertEquals(test.hasSrcAddr, 1); + assertEquals(test.prefixMatchLen, 16 * 8); + assertEquals(test.labelMatch, 1); + assertEquals(test.scopeMatch, 1); + assertEquals(test.scope, IPV6_ADDR_SCOPE_LINKLOCAL); + assertEquals(test.label, 0); + assertEquals(test.precedence, 50); + + // Test linklocal address + test = makeSortableAddress("fe80::c46f:1cff:fe04:39b4"); + assertEquals(test.scope, IPV6_ADDR_SCOPE_LINKLOCAL); + assertEquals(test.label, 1); + assertEquals(test.precedence, 40); + + // Test linklocal address + test = makeSortableAddress("fe80::"); + assertEquals(test.scope, IPV6_ADDR_SCOPE_LINKLOCAL); + assertEquals(test.label, 1); + assertEquals(test.precedence, 40); + + // Test 6to4 address + test = makeSortableAddress("2002:c000:0204::"); + assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL); + assertEquals(test.label, 2); + assertEquals(test.precedence, 30); + + // Test unique local address + test = makeSortableAddress("fc00::c000:13ab"); + assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL); + assertEquals(test.label, 13); + assertEquals(test.precedence, 3); + + // Test teredo tunneling address + test = makeSortableAddress("2001::47c1"); + assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL); + assertEquals(test.label, 5); + assertEquals(test.precedence, 5); + + // Test IPv4-compatible addresses + test = makeSortableAddress("::216.58.200.36"); + assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL); + assertEquals(test.label, 3); + assertEquals(test.precedence, 1); + + // Test site-local address + test = makeSortableAddress("fec0::cafe:3ab2"); + assertEquals(test.scope, IPV6_ADDR_SCOPE_SITELOCAL); + assertEquals(test.label, 11); + assertEquals(test.precedence, 1); + + // Test 6bone address + test = makeSortableAddress("3ffe::1234:5678"); + assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL); + assertEquals(test.label, 12); + assertEquals(test.precedence, 1); + } +} diff --git a/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt b/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt new file mode 100644 index 0000000000..b62bdbcfb5 --- /dev/null +++ b/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt @@ -0,0 +1,145 @@ +/* + * 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.util + +import android.content.Context +import android.content.res.Resources +import android.net.ConnectivityResources +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.MAX_TRANSPORT +import android.net.NetworkCapabilities.TRANSPORT_CELLULAR +import android.net.NetworkCapabilities.TRANSPORT_ETHERNET +import android.net.NetworkCapabilities.TRANSPORT_VPN +import android.net.NetworkCapabilities.TRANSPORT_WIFI +import androidx.test.filters.SmallTest +import com.android.internal.R +import org.junit.After +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.fail +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.any +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock + +/** + * Tests for [KeepaliveUtils]. + * + * Build, install and run with: + * atest android.net.util.KeepaliveUtilsTest + */ +@RunWith(JUnit4::class) +@SmallTest +class KeepaliveUtilsTest { + + // Prepare mocked context with given resource strings. + private fun getMockedContextWithStringArrayRes( + id: Int, + name: String, + res: Array? + ): Context { + val mockRes = mock(Resources::class.java) + doReturn(res).`when`(mockRes).getStringArray(eq(id)) + doReturn(id).`when`(mockRes).getIdentifier(eq(name), any(), any()) + + return mock(Context::class.java).apply { + doReturn(mockRes).`when`(this).getResources() + ConnectivityResources.setResourcesContextForTest(this) + } + } + + @After + fun tearDown() { + ConnectivityResources.setResourcesContextForTest(null) + } + + @Test + fun testGetSupportedKeepalives() { + fun assertRunWithException(res: Array?) { + try { + val mockContext = getMockedContextWithStringArrayRes( + R.array.config_networkSupportedKeepaliveCount, + "config_networkSupportedKeepaliveCount", res) + KeepaliveUtils.getSupportedKeepalives(mockContext) + fail("Expected KeepaliveDeviceConfigurationException") + } catch (expected: KeepaliveUtils.KeepaliveDeviceConfigurationException) { + } + } + + // Check resource with various invalid format. + assertRunWithException(null) + assertRunWithException(arrayOf(null)) + assertRunWithException(arrayOfNulls(10)) + assertRunWithException(arrayOf("")) + assertRunWithException(arrayOf("3,ABC")) + assertRunWithException(arrayOf("6,3,3")) + assertRunWithException(arrayOf("5")) + + // Check resource with invalid slots value. + assertRunWithException(arrayOf("3,-1")) + + // Check resource with invalid transport type. + assertRunWithException(arrayOf("-1,3")) + assertRunWithException(arrayOf("10,3")) + + // Check valid customization generates expected array. + val validRes = arrayOf("0,3", "1,0", "4,4") + val expectedValidRes = intArrayOf(3, 0, 0, 0, 4, 0, 0, 0) + + val mockContext = getMockedContextWithStringArrayRes( + R.array.config_networkSupportedKeepaliveCount, + "config_networkSupportedKeepaliveCount", validRes) + val actual = KeepaliveUtils.getSupportedKeepalives(mockContext) + assertArrayEquals(expectedValidRes, actual) + } + + @Test + fun testGetSupportedKeepalivesForNetworkCapabilities() { + // Mock customized supported keepalives for each transport type, and assuming: + // 3 for cellular, + // 6 for wifi, + // 0 for others. + val cust = IntArray(MAX_TRANSPORT + 1).apply { + this[TRANSPORT_CELLULAR] = 3 + this[TRANSPORT_WIFI] = 6 + } + + val nc = NetworkCapabilities() + // Check supported keepalives with single transport type. + nc.transportTypes = intArrayOf(TRANSPORT_CELLULAR) + assertEquals(3, KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(cust, nc)) + + // Check supported keepalives with multiple transport types. + nc.transportTypes = intArrayOf(TRANSPORT_WIFI, TRANSPORT_VPN) + assertEquals(0, KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(cust, nc)) + + // Check supported keepalives with non-customized transport type. + nc.transportTypes = intArrayOf(TRANSPORT_ETHERNET) + assertEquals(0, KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(cust, nc)) + + // Check supported keepalives with undefined transport type. + nc.transportTypes = intArrayOf(MAX_TRANSPORT + 1) + try { + KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(cust, nc) + fail("Expected ArrayIndexOutOfBoundsException") + } catch (expected: ArrayIndexOutOfBoundsException) { + } + } +} diff --git a/tests/unit/java/android/net/util/MultinetworkPolicyTrackerTest.kt b/tests/unit/java/android/net/util/MultinetworkPolicyTrackerTest.kt new file mode 100644 index 0000000000..25aa626657 --- /dev/null +++ b/tests/unit/java/android/net/util/MultinetworkPolicyTrackerTest.kt @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2021 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.util + +import android.content.Context +import android.content.res.Resources +import android.net.ConnectivityManager.MULTIPATH_PREFERENCE_HANDOVER +import android.net.ConnectivityManager.MULTIPATH_PREFERENCE_PERFORMANCE +import android.net.ConnectivityManager.MULTIPATH_PREFERENCE_RELIABILITY +import android.net.ConnectivityResources +import android.net.ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI +import android.net.ConnectivitySettingsManager.NETWORK_METERED_MULTIPATH_PREFERENCE +import android.net.util.MultinetworkPolicyTracker.ActiveDataSubscriptionIdListener +import android.provider.Settings +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import android.test.mock.MockContentResolver +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.connectivity.resources.R +import com.android.internal.util.test.FakeSettingsProvider +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.argThat +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.any +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +/** + * Tests for [MultinetworkPolicyTracker]. + * + * Build, install and run with: + * atest android.net.util.MultinetworkPolicyTrackerTest + */ +@RunWith(AndroidJUnit4::class) +@SmallTest +class MultinetworkPolicyTrackerTest { + private val resources = mock(Resources::class.java).also { + doReturn(R.integer.config_networkAvoidBadWifi).`when`(it).getIdentifier( + eq("config_networkAvoidBadWifi"), eq("integer"), any()) + doReturn(0).`when`(it).getInteger(R.integer.config_networkAvoidBadWifi) + } + private val telephonyManager = mock(TelephonyManager::class.java) + private val subscriptionManager = mock(SubscriptionManager::class.java).also { + doReturn(null).`when`(it).getActiveSubscriptionInfo(anyInt()) + } + private val resolver = MockContentResolver().apply { + addProvider(Settings.AUTHORITY, FakeSettingsProvider()) } + private val context = mock(Context::class.java).also { + doReturn(Context.TELEPHONY_SERVICE).`when`(it) + .getSystemServiceName(TelephonyManager::class.java) + doReturn(telephonyManager).`when`(it).getSystemService(Context.TELEPHONY_SERVICE) + doReturn(subscriptionManager).`when`(it) + .getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) + doReturn(resolver).`when`(it).contentResolver + doReturn(resources).`when`(it).resources + doReturn(it).`when`(it).createConfigurationContext(any()) + Settings.Global.putString(resolver, NETWORK_AVOID_BAD_WIFI, "1") + ConnectivityResources.setResourcesContextForTest(it) + } + private val tracker = MultinetworkPolicyTracker(context, null /* handler */) + + private fun assertMultipathPreference(preference: Int) { + Settings.Global.putString(resolver, NETWORK_METERED_MULTIPATH_PREFERENCE, + preference.toString()) + tracker.updateMeteredMultipathPreference() + assertEquals(preference, tracker.meteredMultipathPreference) + } + + @After + fun tearDown() { + ConnectivityResources.setResourcesContextForTest(null) + } + + @Test + fun testUpdateMeteredMultipathPreference() { + assertMultipathPreference(MULTIPATH_PREFERENCE_HANDOVER) + assertMultipathPreference(MULTIPATH_PREFERENCE_RELIABILITY) + assertMultipathPreference(MULTIPATH_PREFERENCE_PERFORMANCE) + } + + @Test + fun testUpdateAvoidBadWifi() { + Settings.Global.putString(resolver, NETWORK_AVOID_BAD_WIFI, "0") + assertTrue(tracker.updateAvoidBadWifi()) + assertFalse(tracker.avoidBadWifi) + + doReturn(1).`when`(resources).getInteger(R.integer.config_networkAvoidBadWifi) + assertTrue(tracker.updateAvoidBadWifi()) + assertTrue(tracker.avoidBadWifi) + } + + @Test + fun testOnActiveDataSubscriptionIdChanged() { + val testSubId = 1000 + val subscriptionInfo = SubscriptionInfo(testSubId, ""/* iccId */, 1/* iccId */, + "TMO"/* displayName */, "TMO"/* carrierName */, 1/* nameSource */, 1/* iconTint */, + "123"/* number */, 1/* roaming */, null/* icon */, "310"/* mcc */, "210"/* mnc */, + ""/* countryIso */, false/* isEmbedded */, null/* nativeAccessRules */, + "1"/* cardString */) + doReturn(subscriptionInfo).`when`(subscriptionManager).getActiveSubscriptionInfo(testSubId) + + // Modify avoidBadWifi and meteredMultipathPreference settings value and local variables in + // MultinetworkPolicyTracker should be also updated after subId changed. + Settings.Global.putString(resolver, NETWORK_AVOID_BAD_WIFI, "0") + Settings.Global.putString(resolver, NETWORK_METERED_MULTIPATH_PREFERENCE, + MULTIPATH_PREFERENCE_PERFORMANCE.toString()) + + val listenerCaptor = ArgumentCaptor.forClass( + ActiveDataSubscriptionIdListener::class.java) + verify(telephonyManager, times(1)) + .registerTelephonyCallback(any(), listenerCaptor.capture()) + val listener = listenerCaptor.value + listener.onActiveDataSubscriptionIdChanged(testSubId) + + // Check it get resource value with test sub id. + verify(subscriptionManager, times(1)).getActiveSubscriptionInfo(testSubId) + verify(context).createConfigurationContext(argThat { it.mcc == 310 && it.mnc == 210 }) + + // Check if avoidBadWifi and meteredMultipathPreference values have been updated. + assertFalse(tracker.avoidBadWifi) + assertEquals(MULTIPATH_PREFERENCE_PERFORMANCE, tracker.meteredMultipathPreference) + } +} diff --git a/tests/unit/java/com/android/internal/net/NetworkUtilsInternalTest.java b/tests/unit/java/com/android/internal/net/NetworkUtilsInternalTest.java new file mode 100644 index 0000000000..3cfecd5529 --- /dev/null +++ b/tests/unit/java/com/android/internal/net/NetworkUtilsInternalTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2015 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.internal.net; + +import static android.system.OsConstants.AF_INET; +import static android.system.OsConstants.AF_INET6; +import static android.system.OsConstants.AF_UNIX; +import static android.system.OsConstants.EPERM; +import static android.system.OsConstants.SOCK_DGRAM; +import static android.system.OsConstants.SOCK_STREAM; + +import static junit.framework.Assert.assertEquals; + +import static org.junit.Assert.fail; + +import android.system.ErrnoException; +import android.system.Os; + +import androidx.test.runner.AndroidJUnit4; + +import libcore.io.IoUtils; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@androidx.test.filters.SmallTest +public class NetworkUtilsInternalTest { + + private static void expectSocketSuccess(String msg, int domain, int type) { + try { + IoUtils.closeQuietly(Os.socket(domain, type, 0)); + } catch (ErrnoException e) { + fail(msg + e.getMessage()); + } + } + + private static void expectSocketPemissionError(String msg, int domain, int type) { + try { + IoUtils.closeQuietly(Os.socket(domain, type, 0)); + fail(msg); + } catch (ErrnoException e) { + assertEquals(msg, e.errno, EPERM); + } + } + + private static void expectHasNetworking() { + expectSocketSuccess("Creating a UNIX socket should not have thrown ErrnoException", + AF_UNIX, SOCK_STREAM); + expectSocketSuccess("Creating a AF_INET socket shouldn't have thrown ErrnoException", + AF_INET, SOCK_DGRAM); + expectSocketSuccess("Creating a AF_INET6 socket shouldn't have thrown ErrnoException", + AF_INET6, SOCK_DGRAM); + } + + private static void expectNoNetworking() { + expectSocketSuccess("Creating a UNIX socket should not have thrown ErrnoException", + AF_UNIX, SOCK_STREAM); + expectSocketPemissionError( + "Creating a AF_INET socket should have thrown ErrnoException(EPERM)", + AF_INET, SOCK_DGRAM); + expectSocketPemissionError( + "Creating a AF_INET6 socket should have thrown ErrnoException(EPERM)", + AF_INET6, SOCK_DGRAM); + } + + @Test + public void testSetAllowNetworkingForProcess() { + expectHasNetworking(); + NetworkUtilsInternal.setAllowNetworkingForProcess(false); + expectNoNetworking(); + NetworkUtilsInternal.setAllowNetworkingForProcess(true); + expectHasNetworking(); + } +} diff --git a/tests/unit/java/com/android/internal/net/VpnProfileTest.java b/tests/unit/java/com/android/internal/net/VpnProfileTest.java new file mode 100644 index 0000000000..46597d19ef --- /dev/null +++ b/tests/unit/java/com/android/internal/net/VpnProfileTest.java @@ -0,0 +1,218 @@ +/* + * 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 com.android.internal.net; + +import static com.android.testutils.ParcelUtils.assertParcelSane; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.net.IpSecAlgorithm; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** Unit tests for {@link VpnProfile}. */ +@SmallTest +@RunWith(JUnit4.class) +public class VpnProfileTest { + private static final String DUMMY_PROFILE_KEY = "Test"; + + private static final int ENCODED_INDEX_AUTH_PARAMS_INLINE = 23; + private static final int ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS = 24; + + @Test + public void testDefaults() throws Exception { + final VpnProfile p = new VpnProfile(DUMMY_PROFILE_KEY); + + assertEquals(DUMMY_PROFILE_KEY, p.key); + assertEquals("", p.name); + assertEquals(VpnProfile.TYPE_PPTP, p.type); + assertEquals("", p.server); + assertEquals("", p.username); + assertEquals("", p.password); + assertEquals("", p.dnsServers); + assertEquals("", p.searchDomains); + assertEquals("", p.routes); + assertTrue(p.mppe); + assertEquals("", p.l2tpSecret); + assertEquals("", p.ipsecIdentifier); + assertEquals("", p.ipsecSecret); + assertEquals("", p.ipsecUserCert); + assertEquals("", p.ipsecCaCert); + assertEquals("", p.ipsecServerCert); + assertEquals(null, p.proxy); + assertTrue(p.getAllowedAlgorithms() != null && p.getAllowedAlgorithms().isEmpty()); + assertFalse(p.isBypassable); + assertFalse(p.isMetered); + assertEquals(1360, p.maxMtu); + assertFalse(p.areAuthParamsInline); + assertFalse(p.isRestrictedToTestNetworks); + } + + private VpnProfile getSampleIkev2Profile(String key) { + final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */); + + p.name = "foo"; + p.type = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS; + p.server = "bar"; + p.username = "baz"; + p.password = "qux"; + p.dnsServers = "8.8.8.8"; + p.searchDomains = ""; + p.routes = "0.0.0.0/0"; + p.mppe = false; + p.l2tpSecret = ""; + p.ipsecIdentifier = "quux"; + p.ipsecSecret = "quuz"; + p.ipsecUserCert = "corge"; + p.ipsecCaCert = "grault"; + p.ipsecServerCert = "garply"; + p.proxy = null; + p.setAllowedAlgorithms( + Arrays.asList( + IpSecAlgorithm.AUTH_CRYPT_AES_GCM, + IpSecAlgorithm.AUTH_HMAC_SHA512, + IpSecAlgorithm.CRYPT_AES_CBC)); + p.isBypassable = true; + p.isMetered = true; + p.maxMtu = 1350; + p.areAuthParamsInline = true; + + // Not saved, but also not compared. + p.saveLogin = true; + + return p; + } + + @Test + public void testEquals() { + assertEquals( + getSampleIkev2Profile(DUMMY_PROFILE_KEY), getSampleIkev2Profile(DUMMY_PROFILE_KEY)); + + final VpnProfile modified = getSampleIkev2Profile(DUMMY_PROFILE_KEY); + modified.maxMtu--; + assertNotEquals(getSampleIkev2Profile(DUMMY_PROFILE_KEY), modified); + } + + @Test + public void testParcelUnparcel() { + assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 23); + } + + @Test + public void testSetInvalidAlgorithmValueDelimiter() { + final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY); + + try { + profile.setAllowedAlgorithms( + Arrays.asList("test" + VpnProfile.VALUE_DELIMITER + "test")); + fail("Expected failure due to value separator in algorithm name"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testSetInvalidAlgorithmListDelimiter() { + final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY); + + try { + profile.setAllowedAlgorithms( + Arrays.asList("test" + VpnProfile.LIST_DELIMITER + "test")); + fail("Expected failure due to value separator in algorithm name"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testEncodeDecode() { + final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY); + final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode()); + assertEquals(profile, decoded); + } + + @Test + public void testEncodeDecodeTooManyValues() { + final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY); + final byte[] tooManyValues = + (new String(profile.encode()) + VpnProfile.VALUE_DELIMITER + "invalid").getBytes(); + + assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooManyValues)); + } + + private String getEncodedDecodedIkev2ProfileMissingValues(int... missingIndices) { + // Sort to ensure when we remove, we can do it from greatest first. + Arrays.sort(missingIndices); + + final String encoded = new String(getSampleIkev2Profile(DUMMY_PROFILE_KEY).encode()); + final List parts = + new ArrayList<>(Arrays.asList(encoded.split(VpnProfile.VALUE_DELIMITER))); + + // Remove from back first to ensure indexing is consistent. + for (int i = missingIndices.length - 1; i >= 0; i--) { + parts.remove(missingIndices[i]); + } + + return String.join(VpnProfile.VALUE_DELIMITER, parts.toArray(new String[0])); + } + + @Test + public void testEncodeDecodeInvalidNumberOfValues() { + final String tooFewValues = + getEncodedDecodedIkev2ProfileMissingValues( + ENCODED_INDEX_AUTH_PARAMS_INLINE, + ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS /* missingIndices */); + + assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes())); + } + + @Test + public void testEncodeDecodeMissingIsRestrictedToTestNetworks() { + final String tooFewValues = + getEncodedDecodedIkev2ProfileMissingValues( + ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS /* missingIndices */); + + // Verify decoding without isRestrictedToTestNetworks defaults to false + final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes()); + assertFalse(decoded.isRestrictedToTestNetworks); + } + + @Test + public void testEncodeDecodeLoginsNotSaved() { + final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY); + profile.saveLogin = false; + + final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode()); + assertNotEquals(profile, decoded); + + // Add the username/password back, everything else must be equal. + decoded.username = profile.username; + decoded.password = profile.password; + assertEquals(profile, decoded); + } +} diff --git a/tests/unit/java/com/android/internal/util/BitUtilsTest.java b/tests/unit/java/com/android/internal/util/BitUtilsTest.java new file mode 100644 index 0000000000..d2fbdce977 --- /dev/null +++ b/tests/unit/java/com/android/internal/util/BitUtilsTest.java @@ -0,0 +1,201 @@ +/* + * 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.internal.util; + +import static com.android.internal.util.BitUtils.bytesToBEInt; +import static com.android.internal.util.BitUtils.bytesToLEInt; +import static com.android.internal.util.BitUtils.getUint16; +import static com.android.internal.util.BitUtils.getUint32; +import static com.android.internal.util.BitUtils.getUint8; +import static com.android.internal.util.BitUtils.packBits; +import static com.android.internal.util.BitUtils.uint16; +import static com.android.internal.util.BitUtils.uint32; +import static com.android.internal.util.BitUtils.uint8; +import static com.android.internal.util.BitUtils.unpackBits; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Random; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class BitUtilsTest { + + @Test + public void testUnsignedByteWideningConversions() { + byte b0 = 0; + byte b1 = 1; + byte bm1 = -1; + assertEquals(0, uint8(b0)); + assertEquals(1, uint8(b1)); + assertEquals(127, uint8(Byte.MAX_VALUE)); + assertEquals(128, uint8(Byte.MIN_VALUE)); + assertEquals(255, uint8(bm1)); + assertEquals(255, uint8((byte)255)); + } + + @Test + public void testUnsignedShortWideningConversions() { + short s0 = 0; + short s1 = 1; + short sm1 = -1; + assertEquals(0, uint16(s0)); + assertEquals(1, uint16(s1)); + assertEquals(32767, uint16(Short.MAX_VALUE)); + assertEquals(32768, uint16(Short.MIN_VALUE)); + assertEquals(65535, uint16(sm1)); + assertEquals(65535, uint16((short)65535)); + } + + @Test + public void testUnsignedShortComposition() { + byte b0 = 0; + byte b1 = 1; + byte b2 = 2; + byte b10 = 10; + byte b16 = 16; + byte b128 = -128; + byte b224 = -32; + byte b255 = -1; + assertEquals(0x0000, uint16(b0, b0)); + assertEquals(0xffff, uint16(b255, b255)); + assertEquals(0x0a01, uint16(b10, b1)); + assertEquals(0x8002, uint16(b128, b2)); + assertEquals(0x01ff, uint16(b1, b255)); + assertEquals(0x80ff, uint16(b128, b255)); + assertEquals(0xe010, uint16(b224, b16)); + } + + @Test + public void testUnsignedIntWideningConversions() { + assertEquals(0, uint32(0)); + assertEquals(1, uint32(1)); + assertEquals(2147483647L, uint32(Integer.MAX_VALUE)); + assertEquals(2147483648L, uint32(Integer.MIN_VALUE)); + assertEquals(4294967295L, uint32(-1)); + assertEquals(4294967295L, uint32((int)4294967295L)); + } + + @Test + public void testBytesToInt() { + assertEquals(0x00000000, bytesToBEInt(bytes(0, 0, 0, 0))); + assertEquals(0xffffffff, bytesToBEInt(bytes(255, 255, 255, 255))); + assertEquals(0x0a000001, bytesToBEInt(bytes(10, 0, 0, 1))); + assertEquals(0x0a000002, bytesToBEInt(bytes(10, 0, 0, 2))); + assertEquals(0x0a001fff, bytesToBEInt(bytes(10, 0, 31, 255))); + assertEquals(0xe0000001, bytesToBEInt(bytes(224, 0, 0, 1))); + + assertEquals(0x00000000, bytesToLEInt(bytes(0, 0, 0, 0))); + assertEquals(0x01020304, bytesToLEInt(bytes(4, 3, 2, 1))); + assertEquals(0xffff0000, bytesToLEInt(bytes(0, 0, 255, 255))); + } + + @Test + public void testUnsignedGetters() { + ByteBuffer b = ByteBuffer.allocate(4); + b.putInt(0xffff); + + assertEquals(0x0, getUint8(b, 0)); + assertEquals(0x0, getUint8(b, 1)); + assertEquals(0xff, getUint8(b, 2)); + assertEquals(0xff, getUint8(b, 3)); + + assertEquals(0x0, getUint16(b, 0)); + assertEquals(0xffff, getUint16(b, 2)); + + b.rewind(); + b.putInt(0xffffffff); + assertEquals(0xffffffffL, getUint32(b, 0)); + } + + @Test + public void testBitsPacking() { + BitPackingTestCase[] testCases = { + new BitPackingTestCase(0, ints()), + new BitPackingTestCase(1, ints(0)), + new BitPackingTestCase(2, ints(1)), + new BitPackingTestCase(3, ints(0, 1)), + new BitPackingTestCase(4, ints(2)), + new BitPackingTestCase(6, ints(1, 2)), + new BitPackingTestCase(9, ints(0, 3)), + new BitPackingTestCase(~Long.MAX_VALUE, ints(63)), + new BitPackingTestCase(~Long.MAX_VALUE + 1, ints(0, 63)), + new BitPackingTestCase(~Long.MAX_VALUE + 2, ints(1, 63)), + }; + for (BitPackingTestCase tc : testCases) { + int[] got = unpackBits(tc.packedBits); + assertTrue( + "unpackBits(" + + tc.packedBits + + "): expected " + + Arrays.toString(tc.bits) + + " but got " + + Arrays.toString(got), + Arrays.equals(tc.bits, got)); + } + for (BitPackingTestCase tc : testCases) { + long got = packBits(tc.bits); + assertEquals( + "packBits(" + + Arrays.toString(tc.bits) + + "): expected " + + tc.packedBits + + " but got " + + got, + tc.packedBits, + got); + } + + long[] moreTestCases = { + 0, 1, -1, 23895, -908235, Long.MAX_VALUE, Long.MIN_VALUE, new Random().nextLong(), + }; + for (long l : moreTestCases) { + assertEquals(l, packBits(unpackBits(l))); + } + } + + static byte[] bytes(int b1, int b2, int b3, int b4) { + return new byte[] {b(b1), b(b2), b(b3), b(b4)}; + } + + static byte b(int i) { + return (byte) i; + } + + static int[] ints(int... array) { + return array; + } + + static class BitPackingTestCase { + final int[] bits; + final long packedBits; + + BitPackingTestCase(long packedBits, int[] bits) { + this.bits = bits; + this.packedBits = packedBits; + } + } +} diff --git a/tests/unit/java/com/android/internal/util/RingBufferTest.java b/tests/unit/java/com/android/internal/util/RingBufferTest.java new file mode 100644 index 0000000000..d06095a690 --- /dev/null +++ b/tests/unit/java/com/android/internal/util/RingBufferTest.java @@ -0,0 +1,178 @@ +/* + * 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.internal.util; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class RingBufferTest { + + @Test + public void testEmptyRingBuffer() { + RingBuffer buffer = new RingBuffer<>(String.class, 100); + + assertArrayEquals(new String[0], buffer.toArray()); + } + + @Test + public void testIncorrectConstructorArguments() { + try { + RingBuffer buffer = new RingBuffer<>(String.class, -10); + fail("Should not be able to create a negative capacity RingBuffer"); + } catch (IllegalArgumentException expected) { + } + + try { + RingBuffer buffer = new RingBuffer<>(String.class, 0); + fail("Should not be able to create a 0 capacity RingBuffer"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testRingBufferWithNoWrapping() { + RingBuffer buffer = new RingBuffer<>(String.class, 100); + + buffer.append("a"); + buffer.append("b"); + buffer.append("c"); + buffer.append("d"); + buffer.append("e"); + + String[] expected = {"a", "b", "c", "d", "e"}; + assertArrayEquals(expected, buffer.toArray()); + } + + @Test + public void testRingBufferWithCapacity1() { + RingBuffer buffer = new RingBuffer<>(String.class, 1); + + buffer.append("a"); + assertArrayEquals(new String[]{"a"}, buffer.toArray()); + + buffer.append("b"); + assertArrayEquals(new String[]{"b"}, buffer.toArray()); + + buffer.append("c"); + assertArrayEquals(new String[]{"c"}, buffer.toArray()); + + buffer.append("d"); + assertArrayEquals(new String[]{"d"}, buffer.toArray()); + + buffer.append("e"); + assertArrayEquals(new String[]{"e"}, buffer.toArray()); + } + + @Test + public void testRingBufferWithWrapping() { + int capacity = 100; + RingBuffer buffer = new RingBuffer<>(String.class, capacity); + + buffer.append("a"); + buffer.append("b"); + buffer.append("c"); + buffer.append("d"); + buffer.append("e"); + + String[] expected1 = {"a", "b", "c", "d", "e"}; + assertArrayEquals(expected1, buffer.toArray()); + + String[] expected2 = new String[capacity]; + int firstIndex = 0; + int lastIndex = capacity - 1; + + expected2[firstIndex] = "e"; + for (int i = 1; i < capacity; i++) { + buffer.append("x"); + expected2[i] = "x"; + } + assertArrayEquals(expected2, buffer.toArray()); + + buffer.append("x"); + expected2[firstIndex] = "x"; + assertArrayEquals(expected2, buffer.toArray()); + + for (int i = 0; i < 10; i++) { + for (String s : expected2) { + buffer.append(s); + } + } + assertArrayEquals(expected2, buffer.toArray()); + + buffer.append("a"); + expected2[lastIndex] = "a"; + assertArrayEquals(expected2, buffer.toArray()); + } + + @Test + public void testGetNextSlot() { + int capacity = 100; + RingBuffer buffer = new RingBuffer<>(DummyClass1.class, capacity); + + final DummyClass1[] actual = new DummyClass1[capacity]; + final DummyClass1[] expected = new DummyClass1[capacity]; + for (int i = 0; i < capacity; ++i) { + final DummyClass1 obj = buffer.getNextSlot(); + obj.x = capacity * i; + actual[i] = obj; + expected[i] = new DummyClass1(); + expected[i].x = capacity * i; + } + assertArrayEquals(expected, buffer.toArray()); + + for (int i = 0; i < capacity; ++i) { + if (actual[i] != buffer.getNextSlot()) { + fail("getNextSlot() should re-use objects if available"); + } + } + + RingBuffer buffer2 = new RingBuffer<>(DummyClass2.class, capacity); + assertNull("getNextSlot() should return null if the object can't be initiated " + + "(No nullary constructor)", buffer2.getNextSlot()); + + RingBuffer buffer3 = new RingBuffer<>(DummyClass3.class, capacity); + assertNull("getNextSlot() should return null if the object can't be initiated " + + "(Inaccessible class)", buffer3.getNextSlot()); + } + + public static final class DummyClass1 { + int x; + + public boolean equals(Object o) { + if (o instanceof DummyClass1) { + final DummyClass1 other = (DummyClass1) o; + return other.x == this.x; + } + return false; + } + } + + public static final class DummyClass2 { + public DummyClass2(int x) {} + } + + private static final class DummyClass3 {} +} diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java new file mode 100644 index 0000000000..ab50798206 --- /dev/null +++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java @@ -0,0 +1,12461 @@ +/* + * Copyright (C) 2012 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; + +import static android.Manifest.permission.CHANGE_NETWORK_STATE; +import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS; +import static android.Manifest.permission.DUMP; +import static android.Manifest.permission.NETWORK_FACTORY; +import static android.Manifest.permission.NETWORK_SETTINGS; +import static android.app.PendingIntent.FLAG_IMMUTABLE; +import static android.content.Intent.ACTION_PACKAGE_ADDED; +import static android.content.Intent.ACTION_PACKAGE_REMOVED; +import static android.content.Intent.ACTION_PACKAGE_REPLACED; +import static android.content.Intent.ACTION_USER_ADDED; +import static android.content.Intent.ACTION_USER_REMOVED; +import static android.content.Intent.ACTION_USER_UNLOCKED; +import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED; +import static android.content.pm.PackageManager.FEATURE_WIFI; +import static android.content.pm.PackageManager.FEATURE_WIFI_DIRECT; +import static android.content.pm.PackageManager.GET_PERMISSIONS; +import static android.content.pm.PackageManager.MATCH_ANY_USER; +import static android.content.pm.PackageManager.PERMISSION_DENIED; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.net.ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN; +import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER; +import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK; +import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED; +import static android.net.ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER; +import static android.net.ConnectivityManager.BLOCKED_REASON_NONE; +import static android.net.ConnectivityManager.CONNECTIVITY_ACTION; +import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO; +import static android.net.ConnectivityManager.EXTRA_NETWORK_TYPE; +import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT; +import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE; +import static android.net.ConnectivityManager.TYPE_ETHERNET; +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.ConnectivityManager.TYPE_MOBILE_FOTA; +import static android.net.ConnectivityManager.TYPE_MOBILE_MMS; +import static android.net.ConnectivityManager.TYPE_MOBILE_SUPL; +import static android.net.ConnectivityManager.TYPE_PROXY; +import static android.net.ConnectivityManager.TYPE_VPN; +import static android.net.ConnectivityManager.TYPE_WIFI; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; +import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS; +import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_FALLBACK; +import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTP; +import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTPS; +import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_PRIVDNS; +import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL; +import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID; +import static android.net.NetworkCapabilities.NET_CAPABILITY_BIP; +import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL; +import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_EIMS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE; +import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND; +import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA; +import static android.net.NetworkCapabilities.NET_CAPABILITY_IA; +import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; +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_VCN_MANAGED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID; +import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE; +import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY; +import static android.net.NetworkCapabilities.NET_CAPABILITY_RCS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_SUPL; +import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_VSIM; +import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P; +import static android.net.NetworkCapabilities.NET_CAPABILITY_XCAP; +import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION; +import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS; +import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS; +import static android.net.NetworkCapabilities.REDACT_NONE; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; +import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE; +import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID; +import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK; +import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY; +import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY; +import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_UNINITIALIZED; +import static android.net.RouteInfo.RTN_UNREACHABLE; +import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.PREFIX_OPERATION_ADDED; +import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.PREFIX_OPERATION_REMOVED; +import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_FAILURE; +import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_SUCCESS; +import static android.os.Process.INVALID_UID; +import static android.system.OsConstants.IPPROTO_TCP; + +import static com.android.server.ConnectivityServiceTestUtils.transportToLegacyType; +import static com.android.testutils.ConcurrentUtils.await; +import static com.android.testutils.ConcurrentUtils.durationOf; +import static com.android.testutils.ExceptionUtils.ignoreExceptions; +import static com.android.testutils.HandlerUtils.waitForIdleSerialExecutor; +import static com.android.testutils.MiscAsserts.assertContainsAll; +import static com.android.testutils.MiscAsserts.assertContainsExactly; +import static com.android.testutils.MiscAsserts.assertEmpty; +import static com.android.testutils.MiscAsserts.assertLength; +import static com.android.testutils.MiscAsserts.assertRunsInAtMost; +import static com.android.testutils.MiscAsserts.assertThrows; + +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.Assert.fail; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.startsWith; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +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.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.AlarmManager; +import android.app.AppOpsManager; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.usage.NetworkStatsManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.pm.UserInfo; +import android.content.res.Resources; +import android.location.LocationManager; +import android.net.CaptivePortalData; +import android.net.ConnectionInfo; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.ConnectivityManager.PacketKeepalive; +import android.net.ConnectivityManager.PacketKeepaliveCallback; +import android.net.ConnectivityManager.TooManyRequestsException; +import android.net.ConnectivityResources; +import android.net.ConnectivitySettingsManager; +import android.net.ConnectivityThread; +import android.net.DataStallReportParcelable; +import android.net.EthernetManager; +import android.net.IConnectivityDiagnosticsCallback; +import android.net.IDnsResolver; +import android.net.INetd; +import android.net.INetworkMonitor; +import android.net.INetworkMonitorCallbacks; +import android.net.IOnCompleteListener; +import android.net.IQosCallback; +import android.net.InetAddresses; +import android.net.InterfaceConfigurationParcel; +import android.net.IpPrefix; +import android.net.IpSecManager; +import android.net.IpSecManager.UdpEncapsulationSocket; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.MatchAllNetworkSpecifier; +import android.net.NativeNetworkConfig; +import android.net.NativeNetworkType; +import android.net.Network; +import android.net.NetworkAgent; +import android.net.NetworkAgentConfig; +import android.net.NetworkCapabilities; +import android.net.NetworkFactory; +import android.net.NetworkInfo; +import android.net.NetworkInfo.DetailedState; +import android.net.NetworkPolicyManager; +import android.net.NetworkPolicyManager.NetworkPolicyCallback; +import android.net.NetworkRequest; +import android.net.NetworkScore; +import android.net.NetworkSpecifier; +import android.net.NetworkStack; +import android.net.NetworkStackClient; +import android.net.NetworkStateSnapshot; +import android.net.NetworkTestResultParcelable; +import android.net.OemNetworkPreferences; +import android.net.ProxyInfo; +import android.net.QosCallbackException; +import android.net.QosFilter; +import android.net.QosSession; +import android.net.ResolverParamsParcel; +import android.net.RouteInfo; +import android.net.RouteInfoParcel; +import android.net.SocketKeepalive; +import android.net.TransportInfo; +import android.net.UidRange; +import android.net.UidRangeParcel; +import android.net.UnderlyingNetworkInfo; +import android.net.Uri; +import android.net.VpnManager; +import android.net.VpnTransportInfo; +import android.net.metrics.IpConnectivityLog; +import android.net.resolv.aidl.Nat64PrefixEventParcel; +import android.net.resolv.aidl.PrivateDnsValidationEventParcel; +import android.net.shared.NetworkMonitorUtils; +import android.net.shared.PrivateDnsConfig; +import android.net.util.MultinetworkPolicyTracker; +import android.os.BadParcelableException; +import android.os.Binder; +import android.os.Build; +import android.os.Bundle; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.INetworkManagementService; +import android.os.Looper; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.os.SystemClock; +import android.os.SystemConfigManager; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.security.Credentials; +import android.system.Os; +import android.telephony.TelephonyManager; +import android.telephony.data.EpsBearerQosSessionAttributes; +import android.telephony.data.NrQosSessionAttributes; +import android.test.mock.MockContentResolver; +import android.text.TextUtils; +import android.util.ArraySet; +import android.util.Log; +import android.util.Pair; +import android.util.Range; +import android.util.SparseArray; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.connectivity.resources.R; +import com.android.internal.net.VpnConfig; +import com.android.internal.net.VpnProfile; +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.WakeupMessage; +import com.android.internal.util.test.BroadcastInterceptingContext; +import com.android.internal.util.test.FakeSettingsProvider; +import com.android.net.module.util.ArrayTrackRecord; +import com.android.server.ConnectivityService.ConnectivityDiagnosticsCallbackInfo; +import com.android.server.connectivity.ConnectivityConstants; +import com.android.server.connectivity.MockableSystemProperties; +import com.android.server.connectivity.Nat464Xlat; +import com.android.server.connectivity.NetworkAgentInfo; +import com.android.server.connectivity.NetworkNotificationManager.NotificationType; +import com.android.server.connectivity.ProxyTracker; +import com.android.server.connectivity.QosCallbackTracker; +import com.android.server.connectivity.Vpn; +import com.android.server.connectivity.VpnProfileStore; +import com.android.server.net.NetworkPinner; +import com.android.testutils.ExceptionUtils; +import com.android.testutils.HandlerUtils; +import com.android.testutils.RecorderCallback.CallbackEntry; +import com.android.testutils.TestableNetworkCallback; + +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.AdditionalAnswers; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.mockito.stubbing.Answer; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.DatagramSocket; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import kotlin.reflect.KClass; + +/** + * Tests for {@link ConnectivityService}. + * + * Build, install and run with: + * runtest frameworks-net -c com.android.server.ConnectivityServiceTest + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ConnectivityServiceTest { + private static final String TAG = "ConnectivityServiceTest"; + + private static final int TIMEOUT_MS = 500; + // Broadcasts can take a long time to be delivered. The test will not wait for that long unless + // there is a failure, so use a long timeout. + private static final int BROADCAST_TIMEOUT_MS = 30_000; + private static final int TEST_LINGER_DELAY_MS = 400; + private static final int TEST_NASCENT_DELAY_MS = 300; + // Chosen to be less than the linger and nascent timeout. This ensures that we can distinguish + // between a LOST callback that arrives immediately and a LOST callback that arrives after + // the linger/nascent timeout. For this, our assertions should run fast enough to leave + // less than (mService.mLingerDelayMs - TEST_CALLBACK_TIMEOUT_MS) between the time callbacks are + // supposedly fired, and the time we call expectCallback. + private static final int TEST_CALLBACK_TIMEOUT_MS = 250; + // Chosen to be less than TEST_CALLBACK_TIMEOUT_MS. This ensures that requests have time to + // complete before callbacks are verified. + private static final int TEST_REQUEST_TIMEOUT_MS = 150; + + private static final int UNREASONABLY_LONG_ALARM_WAIT_MS = 1000; + + private static final long TIMESTAMP = 1234L; + + private static final int NET_ID = 110; + private static final int OEM_PREF_ANY_NET_ID = -1; + // Set a non-zero value to verify the flow to set tcp init rwnd value. + private static final int TEST_TCP_INIT_RWND = 60; + + // Used for testing the per-work-profile default network. + private static final int TEST_APP_ID = 103; + private static final int TEST_WORK_PROFILE_USER_ID = 2; + private static final int TEST_WORK_PROFILE_APP_UID = + UserHandle.getUid(TEST_WORK_PROFILE_USER_ID, TEST_APP_ID); + private static final String CLAT_PREFIX = "v4-"; + private static final String MOBILE_IFNAME = "test_rmnet_data0"; + private static final String WIFI_IFNAME = "test_wlan0"; + private static final String WIFI_WOL_IFNAME = "test_wlan_wol"; + private static final String VPN_IFNAME = "tun10042"; + private static final String TEST_PACKAGE_NAME = "com.android.test.package"; + private static final int TEST_PACKAGE_UID = 123; + private static final String ALWAYS_ON_PACKAGE = "com.android.test.alwaysonvpn"; + + private static final String INTERFACE_NAME = "interface"; + + private static final String TEST_VENUE_URL_NA_PASSPOINT = "https://android.com/"; + private static final String TEST_VENUE_URL_NA_OTHER = "https://example.com/"; + private static final String TEST_TERMS_AND_CONDITIONS_URL_NA_PASSPOINT = + "https://android.com/terms/"; + private static final String TEST_TERMS_AND_CONDITIONS_URL_NA_OTHER = + "https://example.com/terms/"; + private static final String TEST_VENUE_URL_CAPPORT = "https://android.com/capport/"; + private static final String TEST_USER_PORTAL_API_URL_CAPPORT = + "https://android.com/user/api/capport/"; + private static final String TEST_FRIENDLY_NAME = "Network friendly name"; + private static final String TEST_REDIRECT_URL = "http://example.com/firstPath"; + + private MockContext mServiceContext; + private HandlerThread mCsHandlerThread; + private HandlerThread mVMSHandlerThread; + private ConnectivityService.Dependencies mDeps; + private ConnectivityService mService; + private WrappedConnectivityManager mCm; + private TestNetworkAgentWrapper mWiFiNetworkAgent; + private TestNetworkAgentWrapper mCellNetworkAgent; + private TestNetworkAgentWrapper mEthernetNetworkAgent; + private MockVpn mMockVpn; + private Context mContext; + private NetworkPolicyCallback mPolicyCallback; + private WrappedMultinetworkPolicyTracker mPolicyTracker; + private HandlerThread mAlarmManagerThread; + private TestNetIdManager mNetIdManager; + private QosCallbackMockHelper mQosCallbackMockHelper; + private QosCallbackTracker mQosCallbackTracker; + private VpnManagerService mVpnManagerService; + private TestNetworkCallback mDefaultNetworkCallback; + private TestNetworkCallback mSystemDefaultNetworkCallback; + private TestNetworkCallback mProfileDefaultNetworkCallback; + + // State variables required to emulate NetworkPolicyManagerService behaviour. + private int mBlockedReasons = BLOCKED_REASON_NONE; + + @Mock DeviceIdleInternal mDeviceIdleInternal; + @Mock INetworkManagementService mNetworkManagementService; + @Mock NetworkStatsManager mStatsManager; + @Mock IDnsResolver mMockDnsResolver; + @Mock INetd mMockNetd; + @Mock NetworkStackClient mNetworkStack; + @Mock PackageManager mPackageManager; + @Mock UserManager mUserManager; + @Mock NotificationManager mNotificationManager; + @Mock AlarmManager mAlarmManager; + @Mock IConnectivityDiagnosticsCallback mConnectivityDiagnosticsCallback; + @Mock IBinder mIBinder; + @Mock LocationManager mLocationManager; + @Mock AppOpsManager mAppOpsManager; + @Mock TelephonyManager mTelephonyManager; + @Mock MockableSystemProperties mSystemProperties; + @Mock EthernetManager mEthernetManager; + @Mock NetworkPolicyManager mNetworkPolicyManager; + @Mock VpnProfileStore mVpnProfileStore; + @Mock SystemConfigManager mSystemConfigManager; + @Mock Resources mResources; + + private ArgumentCaptor mResolverParamsParcelCaptor = + ArgumentCaptor.forClass(ResolverParamsParcel.class); + + // This class exists to test bindProcessToNetwork and getBoundNetworkForProcess. These methods + // do not go through ConnectivityService but talk to netd directly, so they don't automatically + // reflect the state of our test ConnectivityService. + private class WrappedConnectivityManager extends ConnectivityManager { + private Network mFakeBoundNetwork; + + public synchronized boolean bindProcessToNetwork(Network network) { + mFakeBoundNetwork = network; + return true; + } + + public synchronized Network getBoundNetworkForProcess() { + return mFakeBoundNetwork; + } + + public WrappedConnectivityManager(Context context, ConnectivityService service) { + super(context, service); + } + } + + private class MockContext extends BroadcastInterceptingContext { + private final MockContentResolver mContentResolver; + + @Spy private Resources mInternalResources; + private final LinkedBlockingQueue mStartedActivities = new LinkedBlockingQueue<>(); + + // Map of permission name -> PermissionManager.Permission_{GRANTED|DENIED} constant + private final HashMap mMockedPermissions = new HashMap<>(); + + MockContext(Context base, ContentProvider settingsProvider) { + super(base); + + mInternalResources = spy(base.getResources()); + when(mInternalResources.getStringArray(com.android.internal.R.array.networkAttributes)) + .thenReturn(new String[] { + "wifi,1,1,1,-1,true", + "mobile,0,0,0,-1,true", + "mobile_mms,2,0,2,60000,true", + "mobile_supl,3,0,2,60000,true", + }); + + mContentResolver = new MockContentResolver(); + mContentResolver.addProvider(Settings.AUTHORITY, settingsProvider); + } + + @Override + public void startActivityAsUser(Intent intent, UserHandle handle) { + mStartedActivities.offer(intent); + } + + public Intent expectStartActivityIntent(int timeoutMs) { + Intent intent = null; + try { + intent = mStartedActivities.poll(timeoutMs, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) {} + assertNotNull("Did not receive sign-in intent after " + timeoutMs + "ms", intent); + return intent; + } + + public void expectNoStartActivityIntent(int timeoutMs) { + try { + assertNull("Received unexpected Intent to start activity", + mStartedActivities.poll(timeoutMs, TimeUnit.MILLISECONDS)); + } catch (InterruptedException e) {} + } + + @Override + public ComponentName startService(Intent service) { + final String action = service.getAction(); + if (!VpnConfig.SERVICE_INTERFACE.equals(action)) { + fail("Attempt to start unknown service, action=" + action); + } + return new ComponentName(service.getPackage(), "com.android.test.Service"); + } + + @Override + public Object getSystemService(String name) { + if (Context.CONNECTIVITY_SERVICE.equals(name)) return mCm; + if (Context.NOTIFICATION_SERVICE.equals(name)) return mNotificationManager; + if (Context.USER_SERVICE.equals(name)) return mUserManager; + if (Context.ALARM_SERVICE.equals(name)) return mAlarmManager; + if (Context.LOCATION_SERVICE.equals(name)) return mLocationManager; + if (Context.APP_OPS_SERVICE.equals(name)) return mAppOpsManager; + if (Context.TELEPHONY_SERVICE.equals(name)) return mTelephonyManager; + if (Context.ETHERNET_SERVICE.equals(name)) return mEthernetManager; + if (Context.NETWORK_POLICY_SERVICE.equals(name)) return mNetworkPolicyManager; + if (Context.SYSTEM_CONFIG_SERVICE.equals(name)) return mSystemConfigManager; + if (Context.NETWORK_STATS_SERVICE.equals(name)) return mStatsManager; + return super.getSystemService(name); + } + + final HashMap mUserManagers = new HashMap<>(); + @Override + public Context createContextAsUser(UserHandle user, int flags) { + final Context asUser = mock(Context.class, AdditionalAnswers.delegatesTo(this)); + doReturn(user).when(asUser).getUser(); + doAnswer((inv) -> { + final UserManager um = mUserManagers.computeIfAbsent(user, + u -> mock(UserManager.class, AdditionalAnswers.delegatesTo(mUserManager))); + return um; + }).when(asUser).getSystemService(Context.USER_SERVICE); + return asUser; + } + + public void setWorkProfile(@NonNull final UserHandle userHandle, boolean value) { + // This relies on all contexts for a given user returning the same UM mock + final UserManager umMock = createContextAsUser(userHandle, 0 /* flags */) + .getSystemService(UserManager.class); + doReturn(value).when(umMock).isManagedProfile(); + doReturn(value).when(mUserManager).isManagedProfile(eq(userHandle.getIdentifier())); + } + + @Override + public ContentResolver getContentResolver() { + return mContentResolver; + } + + @Override + public Resources getResources() { + return mInternalResources; + } + + @Override + public PackageManager getPackageManager() { + return mPackageManager; + } + + private int checkMockedPermission(String permission, Supplier ifAbsent) { + final Integer granted = mMockedPermissions.get(permission); + return granted != null ? granted : ifAbsent.get(); + } + + @Override + public int checkPermission(String permission, int pid, int uid) { + return checkMockedPermission( + permission, () -> super.checkPermission(permission, pid, uid)); + } + + @Override + public int checkCallingOrSelfPermission(String permission) { + return checkMockedPermission( + permission, () -> super.checkCallingOrSelfPermission(permission)); + } + + @Override + public void enforceCallingOrSelfPermission(String permission, String message) { + final Integer granted = mMockedPermissions.get(permission); + if (granted == null) { + super.enforceCallingOrSelfPermission(permission, message); + return; + } + + if (!granted.equals(PERMISSION_GRANTED)) { + throw new SecurityException("[Test] permission denied: " + permission); + } + } + + /** + * Mock checks for the specified permission, and have them behave as per {@code granted}. + * + *

Passing null reverts to default behavior, which does a real permission check on the + * test package. + * @param granted One of {@link PackageManager#PERMISSION_GRANTED} or + * {@link PackageManager#PERMISSION_DENIED}. + */ + public void setPermission(String permission, Integer granted) { + mMockedPermissions.put(permission, granted); + } + } + + private void waitForIdle() { + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + waitForIdle(mCellNetworkAgent, TIMEOUT_MS); + waitForIdle(mWiFiNetworkAgent, TIMEOUT_MS); + waitForIdle(mEthernetNetworkAgent, TIMEOUT_MS); + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + HandlerUtils.waitForIdle(ConnectivityThread.get(), TIMEOUT_MS); + } + + private void waitForIdle(TestNetworkAgentWrapper agent, long timeoutMs) { + if (agent == null) { + return; + } + agent.waitForIdle(timeoutMs); + } + + @Test + public void testWaitForIdle() throws Exception { + final int attempts = 50; // Causes the test to take about 200ms on bullhead-eng. + + // Tests that waitForIdle returns immediately if the service is already idle. + for (int i = 0; i < attempts; i++) { + waitForIdle(); + } + + // Bring up a network that we can use to send messages to ConnectivityService. + ExpectedBroadcast b = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + b.expectBroadcast(); + Network n = mWiFiNetworkAgent.getNetwork(); + assertNotNull(n); + + // Tests that calling waitForIdle waits for messages to be processed. + for (int i = 0; i < attempts; i++) { + mWiFiNetworkAgent.setSignalStrength(i); + waitForIdle(); + assertEquals(i, mCm.getNetworkCapabilities(n).getSignalStrength()); + } + } + + // This test has an inherent race condition in it, and cannot be enabled for continuous testing + // or presubmit tests. It is kept for manual runs and documentation purposes. + @Ignore + public void verifyThatNotWaitingForIdleCausesRaceConditions() throws Exception { + // Bring up a network that we can use to send messages to ConnectivityService. + ExpectedBroadcast b = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + b.expectBroadcast(); + Network n = mWiFiNetworkAgent.getNetwork(); + assertNotNull(n); + + // Ensure that not calling waitForIdle causes a race condition. + final int attempts = 50; // Causes the test to take about 200ms on bullhead-eng. + for (int i = 0; i < attempts; i++) { + mWiFiNetworkAgent.setSignalStrength(i); + if (i != mCm.getNetworkCapabilities(n).getSignalStrength()) { + // We hit a race condition, as expected. Pass the test. + return; + } + } + + // No race? There is a bug in this test. + fail("expected race condition at least once in " + attempts + " attempts"); + } + + private class TestNetworkAgentWrapper extends NetworkAgentWrapper { + private static final int VALIDATION_RESULT_INVALID = 0; + + private static final long DATA_STALL_TIMESTAMP = 10L; + private static final int DATA_STALL_DETECTION_METHOD = 1; + + private INetworkMonitor mNetworkMonitor; + private INetworkMonitorCallbacks mNmCallbacks; + private int mNmValidationResult = VALIDATION_RESULT_INVALID; + private int mProbesCompleted; + private int mProbesSucceeded; + private String mNmValidationRedirectUrl = null; + private boolean mNmProvNotificationRequested = false; + private Runnable mCreatedCallback; + private Runnable mUnwantedCallback; + private Runnable mDisconnectedCallback; + + private final ConditionVariable mNetworkStatusReceived = new ConditionVariable(); + // Contains the redirectUrl from networkStatus(). Before reading, wait for + // mNetworkStatusReceived. + private String mRedirectUrl; + + TestNetworkAgentWrapper(int transport) throws Exception { + this(transport, new LinkProperties(), null); + } + + TestNetworkAgentWrapper(int transport, LinkProperties linkProperties) + throws Exception { + this(transport, linkProperties, null); + } + + private TestNetworkAgentWrapper(int transport, LinkProperties linkProperties, + NetworkCapabilities ncTemplate) throws Exception { + super(transport, linkProperties, ncTemplate, mServiceContext); + + // Waits for the NetworkAgent to be registered, which includes the creation of the + // NetworkMonitor. + waitForIdle(TIMEOUT_MS); + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + HandlerUtils.waitForIdle(ConnectivityThread.get(), TIMEOUT_MS); + } + + @Override + protected InstrumentedNetworkAgent makeNetworkAgent(LinkProperties linkProperties, + NetworkAgentConfig nac) throws Exception { + mNetworkMonitor = mock(INetworkMonitor.class); + + final Answer validateAnswer = inv -> { + new Thread(ignoreExceptions(this::onValidationRequested)).start(); + return null; + }; + + doAnswer(validateAnswer).when(mNetworkMonitor).notifyNetworkConnected(any(), any()); + doAnswer(validateAnswer).when(mNetworkMonitor).forceReevaluation(anyInt()); + + final ArgumentCaptor nmNetworkCaptor = ArgumentCaptor.forClass(Network.class); + final ArgumentCaptor nmCbCaptor = + ArgumentCaptor.forClass(INetworkMonitorCallbacks.class); + doNothing().when(mNetworkStack).makeNetworkMonitor( + nmNetworkCaptor.capture(), + any() /* name */, + nmCbCaptor.capture()); + + final InstrumentedNetworkAgent na = + new InstrumentedNetworkAgent(this, linkProperties, nac) { + @Override + public void networkStatus(int status, String redirectUrl) { + mRedirectUrl = redirectUrl; + mNetworkStatusReceived.open(); + } + + @Override + public void onNetworkCreated() { + super.onNetworkCreated(); + if (mCreatedCallback != null) mCreatedCallback.run(); + } + + @Override + public void onNetworkUnwanted() { + super.onNetworkUnwanted(); + if (mUnwantedCallback != null) mUnwantedCallback.run(); + } + + @Override + public void onNetworkDestroyed() { + super.onNetworkDestroyed(); + if (mDisconnectedCallback != null) mDisconnectedCallback.run(); + } + }; + + assertEquals(na.getNetwork().netId, nmNetworkCaptor.getValue().netId); + mNmCallbacks = nmCbCaptor.getValue(); + + mNmCallbacks.onNetworkMonitorCreated(mNetworkMonitor); + + return na; + } + + private void onValidationRequested() throws Exception { + if (mNmProvNotificationRequested + && ((mNmValidationResult & NETWORK_VALIDATION_RESULT_VALID) != 0)) { + mNmCallbacks.hideProvisioningNotification(); + mNmProvNotificationRequested = false; + } + + mNmCallbacks.notifyProbeStatusChanged(mProbesCompleted, mProbesSucceeded); + final NetworkTestResultParcelable p = new NetworkTestResultParcelable(); + p.result = mNmValidationResult; + p.probesAttempted = mProbesCompleted; + p.probesSucceeded = mProbesSucceeded; + p.redirectUrl = mNmValidationRedirectUrl; + p.timestampMillis = TIMESTAMP; + mNmCallbacks.notifyNetworkTestedWithExtras(p); + + if (mNmValidationRedirectUrl != null) { + mNmCallbacks.showProvisioningNotification( + "test_provisioning_notif_action", TEST_PACKAGE_NAME); + mNmProvNotificationRequested = true; + } + } + + /** + * Connect without adding any internet capability. + */ + public void connectWithoutInternet() { + super.connect(); + } + + /** + * Transition this NetworkAgent to CONNECTED state with NET_CAPABILITY_INTERNET. + * @param validated Indicate if network should pretend to be validated. + */ + public void connect(boolean validated) { + connect(validated, true, false /* isStrictMode */); + } + + /** + * Transition this NetworkAgent to CONNECTED state. + * @param validated Indicate if network should pretend to be validated. + * @param hasInternet Indicate if network should pretend to have NET_CAPABILITY_INTERNET. + */ + public void connect(boolean validated, boolean hasInternet, boolean isStrictMode) { + assertFalse(getNetworkCapabilities().hasCapability(NET_CAPABILITY_INTERNET)); + + ConnectivityManager.NetworkCallback callback = null; + final ConditionVariable validatedCv = new ConditionVariable(); + if (validated) { + setNetworkValid(isStrictMode); + NetworkRequest request = new NetworkRequest.Builder() + .addTransportType(getNetworkCapabilities().getTransportTypes()[0]) + .clearCapabilities() + .build(); + callback = new ConnectivityManager.NetworkCallback() { + public void onCapabilitiesChanged(Network network, + NetworkCapabilities networkCapabilities) { + if (network.equals(getNetwork()) && + networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED)) { + validatedCv.open(); + } + } + }; + mCm.registerNetworkCallback(request, callback); + } + if (hasInternet) { + addCapability(NET_CAPABILITY_INTERNET); + } + + connectWithoutInternet(); + + if (validated) { + // Wait for network to validate. + waitFor(validatedCv); + setNetworkInvalid(isStrictMode); + } + + if (callback != null) mCm.unregisterNetworkCallback(callback); + } + + public void connectWithCaptivePortal(String redirectUrl, boolean isStrictMode) { + setNetworkPortal(redirectUrl, isStrictMode); + connect(false, true /* hasInternet */, isStrictMode); + } + + public void connectWithPartialConnectivity() { + setNetworkPartial(); + connect(false); + } + + public void connectWithPartialValidConnectivity(boolean isStrictMode) { + setNetworkPartialValid(isStrictMode); + connect(false, true /* hasInternet */, isStrictMode); + } + + void setNetworkValid(boolean isStrictMode) { + mNmValidationResult = NETWORK_VALIDATION_RESULT_VALID; + mNmValidationRedirectUrl = null; + int probesSucceeded = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS; + if (isStrictMode) { + probesSucceeded |= NETWORK_VALIDATION_PROBE_PRIVDNS; + } + // The probesCompleted equals to probesSucceeded for the case of valid network, so put + // the same value into two different parameter of the method. + setProbesStatus(probesSucceeded, probesSucceeded); + } + + void setNetworkInvalid(boolean isStrictMode) { + mNmValidationResult = VALIDATION_RESULT_INVALID; + mNmValidationRedirectUrl = null; + int probesCompleted = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS + | NETWORK_VALIDATION_PROBE_HTTP; + int probesSucceeded = 0; + // If the isStrictMode is true, it means the network is invalid when NetworkMonitor + // tried to validate the private DNS but failed. + if (isStrictMode) { + probesCompleted &= ~NETWORK_VALIDATION_PROBE_HTTP; + probesSucceeded = probesCompleted; + probesCompleted |= NETWORK_VALIDATION_PROBE_PRIVDNS; + } + setProbesStatus(probesCompleted, probesSucceeded); + } + + void setNetworkPortal(String redirectUrl, boolean isStrictMode) { + setNetworkInvalid(isStrictMode); + mNmValidationRedirectUrl = redirectUrl; + // Suppose the portal is found when NetworkMonitor probes NETWORK_VALIDATION_PROBE_HTTP + // in the beginning, so the NETWORK_VALIDATION_PROBE_HTTPS hasn't probed yet. + int probesCompleted = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTP; + int probesSucceeded = VALIDATION_RESULT_INVALID; + if (isStrictMode) { + probesCompleted |= NETWORK_VALIDATION_PROBE_PRIVDNS; + } + setProbesStatus(probesCompleted, probesSucceeded); + } + + void setNetworkPartial() { + mNmValidationResult = NETWORK_VALIDATION_RESULT_PARTIAL; + mNmValidationRedirectUrl = null; + int probesCompleted = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS + | NETWORK_VALIDATION_PROBE_FALLBACK; + int probesSucceeded = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_FALLBACK; + setProbesStatus(probesCompleted, probesSucceeded); + } + + void setNetworkPartialValid(boolean isStrictMode) { + setNetworkPartial(); + mNmValidationResult |= NETWORK_VALIDATION_RESULT_VALID; + int probesCompleted = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS + | NETWORK_VALIDATION_PROBE_HTTP; + int probesSucceeded = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTP; + // Suppose the partial network cannot pass the private DNS validation as well, so only + // add NETWORK_VALIDATION_PROBE_DNS in probesCompleted but not probesSucceeded. + if (isStrictMode) { + probesCompleted |= NETWORK_VALIDATION_PROBE_PRIVDNS; + } + setProbesStatus(probesCompleted, probesSucceeded); + } + + void setProbesStatus(int probesCompleted, int probesSucceeded) { + mProbesCompleted = probesCompleted; + mProbesSucceeded = probesSucceeded; + } + + void notifyCapportApiDataChanged(CaptivePortalData data) { + try { + mNmCallbacks.notifyCaptivePortalDataChanged(data); + } catch (RemoteException e) { + throw new AssertionError("This cannot happen", e); + } + } + + public String waitForRedirectUrl() { + assertTrue(mNetworkStatusReceived.block(TIMEOUT_MS)); + return mRedirectUrl; + } + + public void expectDisconnected() { + expectDisconnected(TIMEOUT_MS); + } + + public void expectPreventReconnectReceived() { + expectPreventReconnectReceived(TIMEOUT_MS); + } + + void notifyDataStallSuspected() throws Exception { + final DataStallReportParcelable p = new DataStallReportParcelable(); + p.detectionMethod = DATA_STALL_DETECTION_METHOD; + p.timestampMillis = DATA_STALL_TIMESTAMP; + mNmCallbacks.notifyDataStallSuspected(p); + } + + public void setCreatedCallback(Runnable r) { + mCreatedCallback = r; + } + + public void setUnwantedCallback(Runnable r) { + mUnwantedCallback = r; + } + + public void setDisconnectedCallback(Runnable r) { + mDisconnectedCallback = r; + } + } + + /** + * A NetworkFactory that allows to wait until any in-flight NetworkRequest add or remove + * operations have been processed and test for them. + */ + private static class MockNetworkFactory extends NetworkFactory { + private final ConditionVariable mNetworkStartedCV = new ConditionVariable(); + private final ConditionVariable mNetworkStoppedCV = new ConditionVariable(); + private final AtomicBoolean mNetworkStarted = new AtomicBoolean(false); + + static class RequestEntry { + @NonNull + public final NetworkRequest request; + + RequestEntry(@NonNull final NetworkRequest request) { + this.request = request; + } + + static final class Add extends RequestEntry { + public final int factorySerialNumber; + + Add(@NonNull final NetworkRequest request, final int factorySerialNumber) { + super(request); + this.factorySerialNumber = factorySerialNumber; + } + } + + static final class Remove extends RequestEntry { + Remove(@NonNull final NetworkRequest request) { + super(request); + } + } + } + + // History of received requests adds and removes. + private final ArrayTrackRecord.ReadHead mRequestHistory = + new ArrayTrackRecord().newReadHead(); + + private static T failIfNull(@Nullable final T obj, @Nullable final String message) { + if (null == obj) fail(null != message ? message : "Must not be null"); + return obj; + } + + + public RequestEntry.Add expectRequestAdd() { + return failIfNull((RequestEntry.Add) mRequestHistory.poll(TIMEOUT_MS, + it -> it instanceof RequestEntry.Add), "Expected request add"); + } + + public void expectRequestAdds(final int count) { + for (int i = count; i > 0; --i) { + expectRequestAdd(); + } + } + + public RequestEntry.Remove expectRequestRemove() { + return failIfNull((RequestEntry.Remove) mRequestHistory.poll(TIMEOUT_MS, + it -> it instanceof RequestEntry.Remove), "Expected request remove"); + } + + public void expectRequestRemoves(final int count) { + for (int i = count; i > 0; --i) { + expectRequestRemove(); + } + } + + // Used to collect the networks requests managed by this factory. This is a duplicate of + // the internal information stored in the NetworkFactory (which is private). + private SparseArray mNetworkRequests = new SparseArray<>(); + private final HandlerThread mHandlerSendingRequests; + + public MockNetworkFactory(Looper looper, Context context, String logTag, + NetworkCapabilities filter, HandlerThread threadSendingRequests) { + super(looper, context, logTag, filter); + mHandlerSendingRequests = threadSendingRequests; + } + + public int getMyRequestCount() { + return getRequestCount(); + } + + protected void startNetwork() { + mNetworkStarted.set(true); + mNetworkStartedCV.open(); + } + + protected void stopNetwork() { + mNetworkStarted.set(false); + mNetworkStoppedCV.open(); + } + + public boolean getMyStartRequested() { + return mNetworkStarted.get(); + } + + public ConditionVariable getNetworkStartedCV() { + mNetworkStartedCV.close(); + return mNetworkStartedCV; + } + + public ConditionVariable getNetworkStoppedCV() { + mNetworkStoppedCV.close(); + return mNetworkStoppedCV; + } + + @Override + protected void handleAddRequest(NetworkRequest request, int score, + int factorySerialNumber) { + mNetworkRequests.put(request.requestId, request); + super.handleAddRequest(request, score, factorySerialNumber); + mRequestHistory.add(new RequestEntry.Add(request, factorySerialNumber)); + } + + @Override + protected void handleRemoveRequest(NetworkRequest request) { + mNetworkRequests.remove(request.requestId); + super.handleRemoveRequest(request); + mRequestHistory.add(new RequestEntry.Remove(request)); + } + + public void assertRequestCountEquals(final int count) { + assertEquals(count, getMyRequestCount()); + } + + @Override + public void terminate() { + super.terminate(); + // Make sure there are no remaining requests unaccounted for. + HandlerUtils.waitForIdle(mHandlerSendingRequests, TIMEOUT_MS); + assertNull(mRequestHistory.poll(0, r -> true)); + } + + // Trigger releasing the request as unfulfillable + public void triggerUnfulfillable(NetworkRequest r) { + super.releaseRequestAsUnfulfillableByAnyFactory(r); + } + + public void assertNoRequestChanged() { + assertNull(mRequestHistory.poll(0, r -> true)); + } + } + + private Set uidRangesForUids(int... uids) { + final ArraySet ranges = new ArraySet<>(); + for (final int uid : uids) { + ranges.add(new UidRange(uid, uid)); + } + return ranges; + } + + private static Looper startHandlerThreadAndReturnLooper() { + final HandlerThread handlerThread = new HandlerThread("MockVpnThread"); + handlerThread.start(); + return handlerThread.getLooper(); + } + + private class MockVpn extends Vpn implements TestableNetworkCallback.HasNetwork { + // Careful ! This is different from mNetworkAgent, because MockNetworkAgent does + // not inherit from NetworkAgent. + private TestNetworkAgentWrapper mMockNetworkAgent; + private boolean mAgentRegistered = false; + + private int mVpnType = VpnManager.TYPE_VPN_SERVICE; + private UnderlyingNetworkInfo mUnderlyingNetworkInfo; + + // These ConditionVariables allow tests to wait for LegacyVpnRunner to be stopped/started. + // TODO: this scheme is ad-hoc and error-prone because it does not fail if, for example, the + // test expects two starts in a row, or even if the production code calls start twice in a + // row. find a better solution. Simply putting a method to create a LegacyVpnRunner into + // Vpn.Dependencies doesn't work because LegacyVpnRunner is not a static class and has + // extensive access into the internals of Vpn. + private ConditionVariable mStartLegacyVpnCv = new ConditionVariable(); + private ConditionVariable mStopVpnRunnerCv = new ConditionVariable(); + + public MockVpn(int userId) { + super(startHandlerThreadAndReturnLooper(), mServiceContext, + new Dependencies() { + @Override + public boolean isCallerSystem() { + return true; + } + + @Override + public DeviceIdleInternal getDeviceIdleInternal() { + return mDeviceIdleInternal; + } + }, + mNetworkManagementService, mMockNetd, userId, mVpnProfileStore); + } + + public void setUids(Set uids) { + mNetworkCapabilities.setUids(UidRange.toIntRanges(uids)); + if (mAgentRegistered) { + mMockNetworkAgent.setNetworkCapabilities(mNetworkCapabilities, true); + } + } + + public void setVpnType(int vpnType) { + mVpnType = vpnType; + } + + @Override + public Network getNetwork() { + return (mMockNetworkAgent == null) ? null : mMockNetworkAgent.getNetwork(); + } + + @Override + public int getActiveVpnType() { + return mVpnType; + } + + private LinkProperties makeLinkProperties() { + final LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(VPN_IFNAME); + return lp; + } + + private void registerAgent(boolean isAlwaysMetered, Set uids, LinkProperties lp) + throws Exception { + if (mAgentRegistered) throw new IllegalStateException("already registered"); + updateState(NetworkInfo.DetailedState.CONNECTING, "registerAgent"); + mConfig = new VpnConfig(); + mConfig.session = "MySession12345"; + setUids(uids); + if (!isAlwaysMetered) mNetworkCapabilities.addCapability(NET_CAPABILITY_NOT_METERED); + mInterface = VPN_IFNAME; + mNetworkCapabilities.setTransportInfo(new VpnTransportInfo(getActiveVpnType(), + mConfig.session)); + mMockNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_VPN, lp, + mNetworkCapabilities); + mMockNetworkAgent.waitForIdle(TIMEOUT_MS); + + verify(mMockNetd, times(1)).networkAddUidRanges(eq(mMockVpn.getNetwork().getNetId()), + eq(toUidRangeStableParcels(uids))); + verify(mMockNetd, never()) + .networkRemoveUidRanges(eq(mMockVpn.getNetwork().getNetId()), any()); + mAgentRegistered = true; + verify(mMockNetd).networkCreate(nativeNetworkConfigVpn(getNetwork().netId, + !mMockNetworkAgent.isBypassableVpn(), mVpnType)); + updateState(NetworkInfo.DetailedState.CONNECTED, "registerAgent"); + mNetworkCapabilities.set(mMockNetworkAgent.getNetworkCapabilities()); + mNetworkAgent = mMockNetworkAgent.getNetworkAgent(); + } + + private void registerAgent(Set uids) throws Exception { + registerAgent(false /* isAlwaysMetered */, uids, makeLinkProperties()); + } + + private void connect(boolean validated, boolean hasInternet, boolean isStrictMode) { + mMockNetworkAgent.connect(validated, hasInternet, isStrictMode); + } + + private void connect(boolean validated) { + mMockNetworkAgent.connect(validated); + } + + private TestNetworkAgentWrapper getAgent() { + return mMockNetworkAgent; + } + + public void establish(LinkProperties lp, int uid, Set ranges, boolean validated, + boolean hasInternet, boolean isStrictMode) throws Exception { + mNetworkCapabilities.setOwnerUid(uid); + mNetworkCapabilities.setAdministratorUids(new int[]{uid}); + registerAgent(false, ranges, lp); + connect(validated, hasInternet, isStrictMode); + waitForIdle(); + } + + public void establish(LinkProperties lp, int uid, Set ranges) throws Exception { + establish(lp, uid, ranges, true, true, false); + } + + public void establishForMyUid(LinkProperties lp) throws Exception { + final int uid = Process.myUid(); + establish(lp, uid, uidRangesForUids(uid), true, true, false); + } + + public void establishForMyUid(boolean validated, boolean hasInternet, boolean isStrictMode) + throws Exception { + final int uid = Process.myUid(); + establish(makeLinkProperties(), uid, uidRangesForUids(uid), validated, hasInternet, + isStrictMode); + } + + public void establishForMyUid() throws Exception { + establishForMyUid(makeLinkProperties()); + } + + public void sendLinkProperties(LinkProperties lp) { + mMockNetworkAgent.sendLinkProperties(lp); + } + + public void disconnect() { + if (mMockNetworkAgent != null) { + mMockNetworkAgent.disconnect(); + updateState(NetworkInfo.DetailedState.DISCONNECTED, "disconnect"); + } + mAgentRegistered = false; + setUids(null); + // Remove NET_CAPABILITY_INTERNET or MockNetworkAgent will refuse to connect later on. + mNetworkCapabilities.removeCapability(NET_CAPABILITY_INTERNET); + mInterface = null; + } + + @Override + public void startLegacyVpnRunner() { + mStartLegacyVpnCv.open(); + } + + public void expectStartLegacyVpnRunner() { + assertTrue("startLegacyVpnRunner not called after " + TIMEOUT_MS + " ms", + mStartLegacyVpnCv.block(TIMEOUT_MS)); + + // startLegacyVpn calls stopVpnRunnerPrivileged, which will open mStopVpnRunnerCv, just + // before calling startLegacyVpnRunner. Restore mStopVpnRunnerCv, so the test can expect + // that the VpnRunner is stopped and immediately restarted by calling + // expectStartLegacyVpnRunner() and expectStopVpnRunnerPrivileged() back-to-back. + mStopVpnRunnerCv = new ConditionVariable(); + } + + @Override + public void stopVpnRunnerPrivileged() { + if (mVpnRunner != null) { + super.stopVpnRunnerPrivileged(); + disconnect(); + mStartLegacyVpnCv = new ConditionVariable(); + } + mVpnRunner = null; + mStopVpnRunnerCv.open(); + } + + public void expectStopVpnRunnerPrivileged() { + assertTrue("stopVpnRunnerPrivileged not called after " + TIMEOUT_MS + " ms", + mStopVpnRunnerCv.block(TIMEOUT_MS)); + } + + @Override + public synchronized UnderlyingNetworkInfo getUnderlyingNetworkInfo() { + if (mUnderlyingNetworkInfo != null) return mUnderlyingNetworkInfo; + + return super.getUnderlyingNetworkInfo(); + } + + private synchronized void setUnderlyingNetworkInfo( + UnderlyingNetworkInfo underlyingNetworkInfo) { + mUnderlyingNetworkInfo = underlyingNetworkInfo; + } + } + + private UidRangeParcel[] toUidRangeStableParcels(final @NonNull Set ranges) { + return ranges.stream().map( + r -> new UidRangeParcel(r.start, r.stop)).toArray(UidRangeParcel[]::new); + } + + private VpnManagerService makeVpnManagerService() { + final VpnManagerService.Dependencies deps = new VpnManagerService.Dependencies() { + public int getCallingUid() { + return mDeps.getCallingUid(); + } + + public HandlerThread makeHandlerThread() { + return mVMSHandlerThread; + } + + @Override + public VpnProfileStore getVpnProfileStore() { + return mVpnProfileStore; + } + + public INetd getNetd() { + return mMockNetd; + } + + public INetworkManagementService getINetworkManagementService() { + return mNetworkManagementService; + } + }; + return new VpnManagerService(mServiceContext, deps); + } + + private void assertVpnTransportInfo(NetworkCapabilities nc, int type) { + assertNotNull(nc); + final TransportInfo ti = nc.getTransportInfo(); + assertTrue("VPN TransportInfo is not a VpnTransportInfo: " + ti, + ti instanceof VpnTransportInfo); + assertEquals(type, ((VpnTransportInfo) ti).getType()); + + } + + private void processBroadcast(Intent intent) { + mServiceContext.sendBroadcast(intent); + HandlerUtils.waitForIdle(mVMSHandlerThread, TIMEOUT_MS); + waitForIdle(); + } + + private void mockVpn(int uid) { + synchronized (mVpnManagerService.mVpns) { + int userId = UserHandle.getUserId(uid); + mMockVpn = new MockVpn(userId); + // Every running user always has a Vpn in the mVpns array, even if no VPN is running. + mVpnManagerService.mVpns.put(userId, mMockVpn); + } + } + + private void mockUidNetworkingBlocked() { + doAnswer(i -> isUidBlocked(mBlockedReasons, i.getArgument(1)) + ).when(mNetworkPolicyManager).isUidNetworkingBlocked(anyInt(), anyBoolean()); + } + + private boolean isUidBlocked(int blockedReasons, boolean meteredNetwork) { + final int blockedOnAllNetworksReason = (blockedReasons & ~BLOCKED_METERED_REASON_MASK); + if (blockedOnAllNetworksReason != BLOCKED_REASON_NONE) { + return true; + } + if (meteredNetwork) { + return blockedReasons != BLOCKED_REASON_NONE; + } + return false; + } + + private void setBlockedReasonChanged(int blockedReasons) { + mBlockedReasons = blockedReasons; + mPolicyCallback.onUidBlockedReasonChanged(Process.myUid(), blockedReasons); + } + + private Nat464Xlat getNat464Xlat(NetworkAgentWrapper mna) { + return mService.getNetworkAgentInfoForNetwork(mna.getNetwork()).clatd; + } + + private static class WrappedMultinetworkPolicyTracker extends MultinetworkPolicyTracker { + volatile boolean mConfigRestrictsAvoidBadWifi; + volatile int mConfigMeteredMultipathPreference; + + WrappedMultinetworkPolicyTracker(Context c, Handler h, Runnable r) { + super(c, h, r); + } + + @Override + public boolean configRestrictsAvoidBadWifi() { + return mConfigRestrictsAvoidBadWifi; + } + + @Override + public int configMeteredMultipathPreference() { + return mConfigMeteredMultipathPreference; + } + } + + /** + * Wait up to TIMEOUT_MS for {@code conditionVariable} to open. + * Fails if TIMEOUT_MS goes by before {@code conditionVariable} opens. + */ + static private void waitFor(ConditionVariable conditionVariable) { + if (conditionVariable.block(TIMEOUT_MS)) { + return; + } + fail("ConditionVariable was blocked for more than " + TIMEOUT_MS + "ms"); + } + + private T doAsUid(final int uid, @NonNull final Supplier what) { + when(mDeps.getCallingUid()).thenReturn(uid); + try { + return what.get(); + } finally { + returnRealCallingUid(); + } + } + + private void doAsUid(final int uid, @NonNull final Runnable what) { + doAsUid(uid, () -> { + what.run(); return Void.TYPE; + }); + } + + private void registerNetworkCallbackAsUid(NetworkRequest request, NetworkCallback callback, + int uid) { + doAsUid(uid, () -> { + mCm.registerNetworkCallback(request, callback); + }); + } + + private void registerDefaultNetworkCallbackAsUid(@NonNull final NetworkCallback callback, + final int uid) { + doAsUid(uid, () -> { + mCm.registerDefaultNetworkCallback(callback); + waitForIdle(); + }); + } + + private interface ExceptionalRunnable { + void run() throws Exception; + } + + private void withPermission(String permission, ExceptionalRunnable r) throws Exception { + if (mServiceContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) { + r.run(); + return; + } + try { + mServiceContext.setPermission(permission, PERMISSION_GRANTED); + r.run(); + } finally { + mServiceContext.setPermission(permission, PERMISSION_DENIED); + } + } + + private static final int PRIMARY_USER = 0; + private static final UidRange PRIMARY_UIDRANGE = + UidRange.createForUser(UserHandle.of(PRIMARY_USER)); + private static final int APP1_UID = UserHandle.getUid(PRIMARY_USER, 10100); + private static final int APP2_UID = UserHandle.getUid(PRIMARY_USER, 10101); + private static final int VPN_UID = UserHandle.getUid(PRIMARY_USER, 10043); + private static final UserInfo PRIMARY_USER_INFO = new UserInfo(PRIMARY_USER, "", + UserInfo.FLAG_PRIMARY); + private static final UserHandle PRIMARY_USER_HANDLE = new UserHandle(PRIMARY_USER); + + private static final int RESTRICTED_USER = 1; + private static final UserInfo RESTRICTED_USER_INFO = new UserInfo(RESTRICTED_USER, "", + UserInfo.FLAG_RESTRICTED); + static { + RESTRICTED_USER_INFO.restrictedProfileParentId = PRIMARY_USER; + } + + @Before + public void setUp() throws Exception { + mNetIdManager = new TestNetIdManager(); + + mContext = InstrumentationRegistry.getContext(); + + MockitoAnnotations.initMocks(this); + + when(mUserManager.getAliveUsers()).thenReturn(Arrays.asList(PRIMARY_USER_INFO)); + when(mUserManager.getUserHandles(anyBoolean())).thenReturn( + Arrays.asList(PRIMARY_USER_HANDLE)); + when(mUserManager.getUserInfo(PRIMARY_USER)).thenReturn(PRIMARY_USER_INFO); + // canHaveRestrictedProfile does not take a userId. It applies to the userId of the context + // it was started from, i.e., PRIMARY_USER. + when(mUserManager.canHaveRestrictedProfile()).thenReturn(true); + when(mUserManager.getUserInfo(RESTRICTED_USER)).thenReturn(RESTRICTED_USER_INFO); + + final ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.targetSdkVersion = Build.VERSION_CODES.Q; + when(mPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), any())) + .thenReturn(applicationInfo); + when(mPackageManager.getTargetSdkVersion(anyString())) + .thenReturn(applicationInfo.targetSdkVersion); + when(mSystemConfigManager.getSystemPermissionUids(anyString())).thenReturn(new int[0]); + + // InstrumentationTestRunner prepares a looper, but AndroidJUnitRunner does not. + // http://b/25897652 . + if (Looper.myLooper() == null) { + Looper.prepare(); + } + mockDefaultPackages(); + mockHasSystemFeature(FEATURE_WIFI, true); + mockHasSystemFeature(FEATURE_WIFI_DIRECT, true); + doReturn(true).when(mTelephonyManager).isDataCapable(); + + FakeSettingsProvider.clearSettingsProvider(); + mServiceContext = new MockContext(InstrumentationRegistry.getContext(), + new FakeSettingsProvider()); + mServiceContext.setUseRegisteredHandlers(true); + + mAlarmManagerThread = new HandlerThread("TestAlarmManager"); + mAlarmManagerThread.start(); + initAlarmManager(mAlarmManager, mAlarmManagerThread.getThreadHandler()); + + mCsHandlerThread = new HandlerThread("TestConnectivityService"); + mVMSHandlerThread = new HandlerThread("TestVpnManagerService"); + mDeps = makeDependencies(); + returnRealCallingUid(); + mService = new ConnectivityService(mServiceContext, + mMockDnsResolver, + mock(IpConnectivityLog.class), + mMockNetd, + mDeps); + mService.mLingerDelayMs = TEST_LINGER_DELAY_MS; + mService.mNascentDelayMs = TEST_NASCENT_DELAY_MS; + verify(mDeps).makeMultinetworkPolicyTracker(any(), any(), any()); + + final ArgumentCaptor policyCallbackCaptor = + ArgumentCaptor.forClass(NetworkPolicyCallback.class); + verify(mNetworkPolicyManager).registerNetworkPolicyCallback(any(), + policyCallbackCaptor.capture()); + mPolicyCallback = policyCallbackCaptor.getValue(); + + // Create local CM before sending system ready so that we can answer + // getSystemService() correctly. + mCm = new WrappedConnectivityManager(InstrumentationRegistry.getContext(), mService); + mService.systemReadyInternal(); + mVpnManagerService = makeVpnManagerService(); + mVpnManagerService.systemReady(); + mockVpn(Process.myUid()); + mCm.bindProcessToNetwork(null); + mQosCallbackTracker = mock(QosCallbackTracker.class); + + // Ensure that the default setting for Captive Portals is used for most tests + setCaptivePortalMode(ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_PROMPT); + setAlwaysOnNetworks(false); + setPrivateDnsSettings(PRIVATE_DNS_MODE_OFF, "ignored.example.com"); + } + + private void returnRealCallingUid() { + doAnswer((invocationOnMock) -> Binder.getCallingUid()).when(mDeps).getCallingUid(); + } + + private ConnectivityService.Dependencies makeDependencies() { + doReturn(false).when(mSystemProperties).getBoolean("ro.radio.noril", false); + final ConnectivityService.Dependencies deps = mock(ConnectivityService.Dependencies.class); + doReturn(mCsHandlerThread).when(deps).makeHandlerThread(); + doReturn(mNetIdManager).when(deps).makeNetIdManager(); + doReturn(mNetworkStack).when(deps).getNetworkStack(); + doReturn(mSystemProperties).when(deps).getSystemProperties(); + doReturn(mock(ProxyTracker.class)).when(deps).makeProxyTracker(any(), any()); + doReturn(true).when(deps).queryUserAccess(anyInt(), any(), any()); + doAnswer(inv -> { + mPolicyTracker = new WrappedMultinetworkPolicyTracker( + inv.getArgument(0), inv.getArgument(1), inv.getArgument(2)); + return mPolicyTracker; + }).when(deps).makeMultinetworkPolicyTracker(any(), any(), any()); + doReturn(true).when(deps).getCellular464XlatEnabled(); + + doReturn(60000).when(mResources).getInteger(R.integer.config_networkTransitionTimeout); + doReturn("").when(mResources).getString(R.string.config_networkCaptivePortalServerUrl); + doReturn(new String[]{ WIFI_WOL_IFNAME }).when(mResources).getStringArray( + R.array.config_wakeonlan_supported_interfaces); + doReturn(new String[] { "0,1", "1,3" }).when(mResources).getStringArray( + R.array.config_networkSupportedKeepaliveCount); + doReturn(new String[0]).when(mResources).getStringArray( + R.array.config_networkNotifySwitches); + doReturn(new int[]{10, 11, 12, 14, 15}).when(mResources).getIntArray( + R.array.config_protectedNetworks); + // We don't test the actual notification value strings, so just return an empty array. + // It doesn't matter what the values are as long as it's not null. + doReturn(new String[0]).when(mResources).getStringArray(R.array.network_switch_type_name); + + doReturn(R.array.config_networkSupportedKeepaliveCount).when(mResources) + .getIdentifier(eq("config_networkSupportedKeepaliveCount"), eq("array"), any()); + doReturn(R.array.network_switch_type_name).when(mResources) + .getIdentifier(eq("network_switch_type_name"), eq("array"), any()); + + + final ConnectivityResources connRes = mock(ConnectivityResources.class); + doReturn(mResources).when(connRes).get(); + doReturn(connRes).when(deps).getResources(any()); + + final Context mockResContext = mock(Context.class); + doReturn(mResources).when(mockResContext).getResources(); + ConnectivityResources.setResourcesContextForTest(mockResContext); + + return deps; + } + + private static void initAlarmManager(final AlarmManager am, final Handler alarmHandler) { + doAnswer(inv -> { + final long when = inv.getArgument(1); + final WakeupMessage wakeupMsg = inv.getArgument(3); + final Handler handler = inv.getArgument(4); + + long delayMs = when - SystemClock.elapsedRealtime(); + if (delayMs < 0) delayMs = 0; + if (delayMs > UNREASONABLY_LONG_ALARM_WAIT_MS) { + fail("Attempting to send msg more than " + UNREASONABLY_LONG_ALARM_WAIT_MS + + "ms into the future: " + delayMs); + } + alarmHandler.postDelayed(() -> handler.post(wakeupMsg::onAlarm), wakeupMsg /* token */, + delayMs); + + return null; + }).when(am).setExact(eq(AlarmManager.ELAPSED_REALTIME_WAKEUP), anyLong(), anyString(), + any(WakeupMessage.class), any()); + + doAnswer(inv -> { + final WakeupMessage wakeupMsg = inv.getArgument(0); + alarmHandler.removeCallbacksAndMessages(wakeupMsg /* token */); + return null; + }).when(am).cancel(any(WakeupMessage.class)); + } + + @After + public void tearDown() throws Exception { + unregisterDefaultNetworkCallbacks(); + maybeTearDownEnterpriseNetwork(); + setAlwaysOnNetworks(false); + if (mCellNetworkAgent != null) { + mCellNetworkAgent.disconnect(); + mCellNetworkAgent = null; + } + if (mWiFiNetworkAgent != null) { + mWiFiNetworkAgent.disconnect(); + mWiFiNetworkAgent = null; + } + if (mEthernetNetworkAgent != null) { + mEthernetNetworkAgent.disconnect(); + mEthernetNetworkAgent = null; + } + + if (mQosCallbackMockHelper != null) { + mQosCallbackMockHelper.tearDown(); + mQosCallbackMockHelper = null; + } + mMockVpn.disconnect(); + waitForIdle(); + + FakeSettingsProvider.clearSettingsProvider(); + ConnectivityResources.setResourcesContextForTest(null); + + mCsHandlerThread.quitSafely(); + mAlarmManagerThread.quitSafely(); + } + + private void mockDefaultPackages() throws Exception { + final String myPackageName = mContext.getPackageName(); + final PackageInfo myPackageInfo = mContext.getPackageManager().getPackageInfo( + myPackageName, PackageManager.GET_PERMISSIONS); + when(mPackageManager.getPackagesForUid(Binder.getCallingUid())).thenReturn( + new String[] {myPackageName}); + when(mPackageManager.getPackageInfoAsUser(eq(myPackageName), anyInt(), + eq(UserHandle.getCallingUserId()))).thenReturn(myPackageInfo); + + when(mPackageManager.getInstalledPackages(eq(GET_PERMISSIONS | MATCH_ANY_USER))).thenReturn( + Arrays.asList(new PackageInfo[] { + buildPackageInfo(/* SYSTEM */ false, APP1_UID), + buildPackageInfo(/* SYSTEM */ false, APP2_UID), + buildPackageInfo(/* SYSTEM */ false, VPN_UID) + })); + + // Create a fake always-on VPN package. + final int userId = UserHandle.getCallingUserId(); + final ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.targetSdkVersion = Build.VERSION_CODES.R; // Always-on supported in N+. + when(mPackageManager.getApplicationInfoAsUser(eq(ALWAYS_ON_PACKAGE), anyInt(), + eq(userId))).thenReturn(applicationInfo); + + // Minimal mocking to keep Vpn#isAlwaysOnPackageSupported happy. + ResolveInfo rInfo = new ResolveInfo(); + rInfo.serviceInfo = new ServiceInfo(); + rInfo.serviceInfo.metaData = new Bundle(); + final List services = Arrays.asList(new ResolveInfo[]{rInfo}); + when(mPackageManager.queryIntentServicesAsUser(any(), eq(PackageManager.GET_META_DATA), + eq(userId))).thenReturn(services); + when(mPackageManager.getPackageUidAsUser(TEST_PACKAGE_NAME, userId)) + .thenReturn(Process.myUid()); + when(mPackageManager.getPackageUidAsUser(ALWAYS_ON_PACKAGE, userId)) + .thenReturn(VPN_UID); + } + + private void verifyActiveNetwork(int transport) { + // Test getActiveNetworkInfo() + assertNotNull(mCm.getActiveNetworkInfo()); + assertEquals(transportToLegacyType(transport), mCm.getActiveNetworkInfo().getType()); + // Test getActiveNetwork() + assertNotNull(mCm.getActiveNetwork()); + assertEquals(mCm.getActiveNetwork(), mCm.getActiveNetworkForUid(Process.myUid())); + if (!NetworkCapabilities.isValidTransport(transport)) { + throw new IllegalStateException("Unknown transport " + transport); + } + switch (transport) { + case TRANSPORT_WIFI: + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + break; + case TRANSPORT_CELLULAR: + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + break; + case TRANSPORT_ETHERNET: + assertEquals(mEthernetNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + break; + default: + break; + } + // Test getNetworkInfo(Network) + assertNotNull(mCm.getNetworkInfo(mCm.getActiveNetwork())); + assertEquals(transportToLegacyType(transport), + mCm.getNetworkInfo(mCm.getActiveNetwork()).getType()); + assertNotNull(mCm.getActiveNetworkInfoForUid(Process.myUid())); + // Test getNetworkCapabilities(Network) + assertNotNull(mCm.getNetworkCapabilities(mCm.getActiveNetwork())); + assertTrue(mCm.getNetworkCapabilities(mCm.getActiveNetwork()).hasTransport(transport)); + } + + private void verifyNoNetwork() { + waitForIdle(); + // Test getActiveNetworkInfo() + assertNull(mCm.getActiveNetworkInfo()); + // Test getActiveNetwork() + assertNull(mCm.getActiveNetwork()); + assertNull(mCm.getActiveNetworkForUid(Process.myUid())); + // Test getAllNetworks() + assertEmpty(mCm.getAllNetworks()); + assertEmpty(mCm.getAllNetworkStateSnapshots()); + } + + /** + * Class to simplify expecting broadcasts using BroadcastInterceptingContext. + * Ensures that the receiver is unregistered after the expected broadcast is received. This + * cannot be done in the BroadcastReceiver itself because BroadcastInterceptingContext runs + * the receivers' receive method while iterating over the list of receivers, and unregistering + * the receiver during iteration throws ConcurrentModificationException. + */ + private class ExpectedBroadcast extends CompletableFuture { + private final BroadcastReceiver mReceiver; + + ExpectedBroadcast(BroadcastReceiver receiver) { + mReceiver = receiver; + } + + public Intent expectBroadcast(int timeoutMs) throws Exception { + try { + return get(timeoutMs, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + fail("Expected broadcast not received after " + timeoutMs + " ms"); + return null; + } finally { + mServiceContext.unregisterReceiver(mReceiver); + } + } + + public Intent expectBroadcast() throws Exception { + return expectBroadcast(BROADCAST_TIMEOUT_MS); + } + + public void expectNoBroadcast(int timeoutMs) throws Exception { + waitForIdle(); + try { + final Intent intent = get(timeoutMs, TimeUnit.MILLISECONDS); + fail("Unexpected broadcast: " + intent.getAction() + " " + intent.getExtras()); + } catch (TimeoutException expected) { + } finally { + mServiceContext.unregisterReceiver(mReceiver); + } + } + } + + /** Expects that {@code count} CONNECTIVITY_ACTION broadcasts are received. */ + private ExpectedBroadcast registerConnectivityBroadcast(final int count) { + return registerConnectivityBroadcastThat(count, intent -> true); + } + + private ExpectedBroadcast registerConnectivityBroadcastThat(final int count, + @NonNull final Predicate filter) { + final IntentFilter intentFilter = new IntentFilter(CONNECTIVITY_ACTION); + // AtomicReference allows receiver to access expected even though it is constructed later. + final AtomicReference expectedRef = new AtomicReference<>(); + final BroadcastReceiver receiver = new BroadcastReceiver() { + private int mRemaining = count; + public void onReceive(Context context, Intent intent) { + final int type = intent.getIntExtra(EXTRA_NETWORK_TYPE, -1); + final NetworkInfo ni = intent.getParcelableExtra(EXTRA_NETWORK_INFO); + Log.d(TAG, "Received CONNECTIVITY_ACTION type=" + type + " ni=" + ni); + if (!filter.test(intent)) return; + if (--mRemaining == 0) { + expectedRef.get().complete(intent); + } + } + }; + final ExpectedBroadcast expected = new ExpectedBroadcast(receiver); + expectedRef.set(expected); + mServiceContext.registerReceiver(receiver, intentFilter); + return expected; + } + + private boolean extraInfoInBroadcastHasExpectedNullness(NetworkInfo ni) { + final DetailedState state = ni.getDetailedState(); + if (state == DetailedState.CONNECTED && ni.getExtraInfo() == null) return false; + // Expect a null extraInfo if the network is CONNECTING, because a CONNECTIVITY_ACTION + // broadcast with a state of CONNECTING only happens due to legacy VPN lockdown, which also + // nulls out extraInfo. + if (state == DetailedState.CONNECTING && ni.getExtraInfo() != null) return false; + // Can't make any assertions about DISCONNECTED broadcasts. When a network actually + // disconnects, disconnectAndDestroyNetwork sets its state to DISCONNECTED and its extraInfo + // to null. But if the DISCONNECTED broadcast is just simulated by LegacyTypeTracker due to + // a network switch, extraInfo will likely be populated. + // This is likely a bug in CS, but likely not one we can fix without impacting apps. + return true; + } + + private ExpectedBroadcast expectConnectivityAction(int type, NetworkInfo.DetailedState state) { + return registerConnectivityBroadcastThat(1, intent -> { + final int actualType = intent.getIntExtra(EXTRA_NETWORK_TYPE, -1); + final NetworkInfo ni = intent.getParcelableExtra(EXTRA_NETWORK_INFO); + return type == actualType + && state == ni.getDetailedState() + && extraInfoInBroadcastHasExpectedNullness(ni); + }); + } + + @Test + public void testNetworkTypes() { + // Ensure that our mocks for the networkAttributes config variable work as expected. If they + // don't, then tests that depend on CONNECTIVITY_ACTION broadcasts for these network types + // will fail. Failing here is much easier to debug. + assertTrue(mCm.isNetworkSupported(TYPE_WIFI)); + assertTrue(mCm.isNetworkSupported(TYPE_MOBILE)); + assertTrue(mCm.isNetworkSupported(TYPE_MOBILE_MMS)); + assertTrue(mCm.isNetworkSupported(TYPE_MOBILE_FOTA)); + assertFalse(mCm.isNetworkSupported(TYPE_PROXY)); + + // Check that TYPE_ETHERNET is supported. Unlike the asserts above, which only validate our + // mocks, this assert exercises the ConnectivityService code path that ensures that + // TYPE_ETHERNET is supported if the ethernet service is running. + assertTrue(mCm.isNetworkSupported(TYPE_ETHERNET)); + } + + @Test + public void testNetworkFeature() throws Exception { + // Connect the cell agent and wait for the connected broadcast. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.addCapability(NET_CAPABILITY_SUPL); + ExpectedBroadcast b = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTED); + mCellNetworkAgent.connect(true); + b.expectBroadcast(); + + // Build legacy request for SUPL. + final NetworkCapabilities legacyCaps = new NetworkCapabilities(); + legacyCaps.addTransportType(TRANSPORT_CELLULAR); + legacyCaps.addCapability(NET_CAPABILITY_SUPL); + final NetworkRequest legacyRequest = new NetworkRequest(legacyCaps, TYPE_MOBILE_SUPL, + ConnectivityManager.REQUEST_ID_UNSET, NetworkRequest.Type.REQUEST); + + // File request, withdraw it and make sure no broadcast is sent + b = registerConnectivityBroadcast(1); + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.requestNetwork(legacyRequest, callback); + callback.expectCallback(CallbackEntry.AVAILABLE, mCellNetworkAgent); + mCm.unregisterNetworkCallback(callback); + b.expectNoBroadcast(800); // 800ms long enough to at least flake if this is sent + + // Disconnect the network and expect mobile disconnected broadcast. + b = expectConnectivityAction(TYPE_MOBILE, DetailedState.DISCONNECTED); + mCellNetworkAgent.disconnect(); + b.expectBroadcast(); + } + + @Test + public void testLingering() throws Exception { + verifyNoNetwork(); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + assertNull(mCm.getActiveNetworkInfo()); + assertNull(mCm.getActiveNetwork()); + // Test bringing up validated cellular. + ExpectedBroadcast b = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTED); + mCellNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + assertLength(2, mCm.getAllNetworks()); + assertTrue(mCm.getAllNetworks()[0].equals(mCm.getActiveNetwork()) || + mCm.getAllNetworks()[1].equals(mCm.getActiveNetwork())); + assertTrue(mCm.getAllNetworks()[0].equals(mWiFiNetworkAgent.getNetwork()) || + mCm.getAllNetworks()[1].equals(mWiFiNetworkAgent.getNetwork())); + // Test bringing up validated WiFi. + b = registerConnectivityBroadcast(2); + mWiFiNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + assertLength(2, mCm.getAllNetworks()); + assertTrue(mCm.getAllNetworks()[0].equals(mCm.getActiveNetwork()) || + mCm.getAllNetworks()[1].equals(mCm.getActiveNetwork())); + assertTrue(mCm.getAllNetworks()[0].equals(mCellNetworkAgent.getNetwork()) || + mCm.getAllNetworks()[1].equals(mCellNetworkAgent.getNetwork())); + // Test cellular linger timeout. + mCellNetworkAgent.expectDisconnected(); + waitForIdle(); + assertLength(1, mCm.getAllNetworks()); + verifyActiveNetwork(TRANSPORT_WIFI); + assertLength(1, mCm.getAllNetworks()); + assertEquals(mCm.getAllNetworks()[0], mCm.getActiveNetwork()); + // Test WiFi disconnect. + b = registerConnectivityBroadcast(1); + mWiFiNetworkAgent.disconnect(); + b.expectBroadcast(); + verifyNoNetwork(); + } + + /** + * Verify a newly created network will be inactive instead of torn down even if no one is + * requesting. + */ + @Test + public void testNewNetworkInactive() throws Exception { + // Create a callback that monitoring the testing network. + final TestNetworkCallback listenCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(new NetworkRequest.Builder().build(), listenCallback); + + // 1. Create a network that is not requested by anyone, and does not satisfy any of the + // default requests. Verify that the network will be inactive instead of torn down. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connectWithoutInternet(); + listenCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + listenCallback.assertNoCallback(); + + // Verify that the network will be torn down after nascent expiry. A small period of time + // is added in case of flakiness. + final int nascentTimeoutMs = + mService.mNascentDelayMs + mService.mNascentDelayMs / 4; + listenCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent, nascentTimeoutMs); + + // 2. Create a network that is satisfied by a request comes later. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connectWithoutInternet(); + listenCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + final NetworkRequest wifiRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).build(); + final TestNetworkCallback wifiCallback = new TestNetworkCallback(); + mCm.requestNetwork(wifiRequest, wifiCallback); + wifiCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + + // Verify that the network will be kept since the request is still satisfied. And is able + // to get disconnected as usual if the request is released after the nascent timer expires. + listenCallback.assertNoCallback(nascentTimeoutMs); + mCm.unregisterNetworkCallback(wifiCallback); + listenCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // 3. Create a network that is satisfied by a request comes later. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connectWithoutInternet(); + listenCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + mCm.requestNetwork(wifiRequest, wifiCallback); + wifiCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + + // Verify that the network will still be torn down after the request gets removed. + mCm.unregisterNetworkCallback(wifiCallback); + listenCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // There is no need to ensure that LOSING is never sent in the common case that the + // network immediately satisfies a request that was already present, because it is already + // verified anywhere whenever {@code TestNetworkCallback#expectAvailable*} is called. + + mCm.unregisterNetworkCallback(listenCallback); + } + + /** + * Verify a newly created network will be inactive and switch to background if only background + * request is satisfied. + */ + @Test + public void testNewNetworkInactive_bgNetwork() throws Exception { + // Create a callback that monitoring the wifi network. + final TestNetworkCallback wifiListenCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).build(), wifiListenCallback); + + // Create callbacks that can monitor background and foreground mobile networks. + // This is done by granting using background networks permission before registration. Thus, + // the service will not add {@code NET_CAPABILITY_FOREGROUND} by default. + grantUsingBackgroundNetworksPermissionForUid(Binder.getCallingUid()); + final TestNetworkCallback bgMobileListenCallback = new TestNetworkCallback(); + final TestNetworkCallback fgMobileListenCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(), bgMobileListenCallback); + mCm.registerNetworkCallback(new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_FOREGROUND).build(), fgMobileListenCallback); + + // Connect wifi, which satisfies default request. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + wifiListenCallback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent); + + // Connect a cellular network, verify that satisfies only the background callback. + setAlwaysOnNetworks(true); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + bgMobileListenCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + fgMobileListenCallback.assertNoCallback(); + assertFalse(isForegroundNetwork(mCellNetworkAgent)); + + mCellNetworkAgent.disconnect(); + bgMobileListenCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + fgMobileListenCallback.assertNoCallback(); + + mCm.unregisterNetworkCallback(wifiListenCallback); + mCm.unregisterNetworkCallback(bgMobileListenCallback); + mCm.unregisterNetworkCallback(fgMobileListenCallback); + } + + @Test + public void testValidatedCellularOutscoresUnvalidatedWiFi() throws Exception { + // Test bringing up unvalidated WiFi + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + ExpectedBroadcast b = registerConnectivityBroadcast(1); + mWiFiNetworkAgent.connect(false); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + // Test bringing up unvalidated cellular + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(false); + waitForIdle(); + verifyActiveNetwork(TRANSPORT_WIFI); + // Test cellular disconnect. + mCellNetworkAgent.disconnect(); + waitForIdle(); + verifyActiveNetwork(TRANSPORT_WIFI); + // Test bringing up validated cellular + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + b = registerConnectivityBroadcast(2); + mCellNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + // Test cellular disconnect. + b = registerConnectivityBroadcast(2); + mCellNetworkAgent.disconnect(); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + // Test WiFi disconnect. + b = registerConnectivityBroadcast(1); + mWiFiNetworkAgent.disconnect(); + b.expectBroadcast(); + verifyNoNetwork(); + } + + @Test + public void testUnvalidatedWifiOutscoresUnvalidatedCellular() throws Exception { + // Test bringing up unvalidated cellular. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + ExpectedBroadcast b = registerConnectivityBroadcast(1); + mCellNetworkAgent.connect(false); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + // Test bringing up unvalidated WiFi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + b = registerConnectivityBroadcast(2); + mWiFiNetworkAgent.connect(false); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + // Test WiFi disconnect. + b = registerConnectivityBroadcast(2); + mWiFiNetworkAgent.disconnect(); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + // Test cellular disconnect. + b = registerConnectivityBroadcast(1); + mCellNetworkAgent.disconnect(); + b.expectBroadcast(); + verifyNoNetwork(); + } + + @Test + public void testUnlingeringDoesNotValidate() throws Exception { + // Test bringing up unvalidated WiFi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + ExpectedBroadcast b = registerConnectivityBroadcast(1); + mWiFiNetworkAgent.connect(false); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability( + NET_CAPABILITY_VALIDATED)); + // Test bringing up validated cellular. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + b = registerConnectivityBroadcast(2); + mCellNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability( + NET_CAPABILITY_VALIDATED)); + // Test cellular disconnect. + b = registerConnectivityBroadcast(2); + mCellNetworkAgent.disconnect(); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + // Unlingering a network should not cause it to be marked as validated. + assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability( + NET_CAPABILITY_VALIDATED)); + } + + @Test + public void testCellularOutscoresWeakWifi() throws Exception { + // Test bringing up validated cellular. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + ExpectedBroadcast b = registerConnectivityBroadcast(1); + mCellNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + // Test bringing up validated WiFi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + b = registerConnectivityBroadcast(2); + mWiFiNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + // Test WiFi getting really weak. + b = registerConnectivityBroadcast(2); + mWiFiNetworkAgent.adjustScore(-11); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + // Test WiFi restoring signal strength. + b = registerConnectivityBroadcast(2); + mWiFiNetworkAgent.adjustScore(11); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + } + + @Test + public void testReapingNetwork() throws Exception { + // Test bringing up WiFi without NET_CAPABILITY_INTERNET. + // Expect it to be torn down immediately because it satisfies no requests. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connectWithoutInternet(); + mWiFiNetworkAgent.expectDisconnected(); + // Test bringing up cellular without NET_CAPABILITY_INTERNET. + // Expect it to be torn down immediately because it satisfies no requests. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mCellNetworkAgent.connectWithoutInternet(); + mCellNetworkAgent.expectDisconnected(); + // Test bringing up validated WiFi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + final ExpectedBroadcast b = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED); + mWiFiNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + // Test bringing up unvalidated cellular. + // Expect it to be torn down because it could never be the highest scoring network + // satisfying the default request even if it validated. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(false); + mCellNetworkAgent.expectDisconnected(); + verifyActiveNetwork(TRANSPORT_WIFI); + mWiFiNetworkAgent.disconnect(); + mWiFiNetworkAgent.expectDisconnected(); + } + + @Test + public void testCellularFallback() throws Exception { + // Test bringing up validated cellular. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + ExpectedBroadcast b = registerConnectivityBroadcast(1); + mCellNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + // Test bringing up validated WiFi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + b = registerConnectivityBroadcast(2); + mWiFiNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + // Reevaluate WiFi (it'll instantly fail DNS). + b = registerConnectivityBroadcast(2); + assertTrue(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability( + NET_CAPABILITY_VALIDATED)); + mCm.reportBadNetwork(mWiFiNetworkAgent.getNetwork()); + // Should quickly fall back to Cellular. + b.expectBroadcast(); + assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability( + NET_CAPABILITY_VALIDATED)); + verifyActiveNetwork(TRANSPORT_CELLULAR); + // Reevaluate cellular (it'll instantly fail DNS). + b = registerConnectivityBroadcast(2); + assertTrue(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability( + NET_CAPABILITY_VALIDATED)); + mCm.reportBadNetwork(mCellNetworkAgent.getNetwork()); + // Should quickly fall back to WiFi. + b.expectBroadcast(); + assertFalse(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability( + NET_CAPABILITY_VALIDATED)); + assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability( + NET_CAPABILITY_VALIDATED)); + verifyActiveNetwork(TRANSPORT_WIFI); + } + + @Test + public void testWiFiFallback() throws Exception { + // Test bringing up unvalidated WiFi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + ExpectedBroadcast b = registerConnectivityBroadcast(1); + mWiFiNetworkAgent.connect(false); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + // Test bringing up validated cellular. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + b = registerConnectivityBroadcast(2); + mCellNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + // Reevaluate cellular (it'll instantly fail DNS). + b = registerConnectivityBroadcast(2); + assertTrue(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability( + NET_CAPABILITY_VALIDATED)); + mCm.reportBadNetwork(mCellNetworkAgent.getNetwork()); + // Should quickly fall back to WiFi. + b.expectBroadcast(); + assertFalse(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability( + NET_CAPABILITY_VALIDATED)); + verifyActiveNetwork(TRANSPORT_WIFI); + } + + @Test + public void testRequiresValidation() { + assertTrue(NetworkMonitorUtils.isValidationRequired( + mCm.getDefaultRequest().networkCapabilities)); + } + + /** + * Utility NetworkCallback for testing. The caller must explicitly test for all the callbacks + * this class receives, by calling expectCallback() exactly once each time a callback is + * received. assertNoCallback may be called at any time. + */ + private class TestNetworkCallback extends TestableNetworkCallback { + TestNetworkCallback() { + super(TEST_CALLBACK_TIMEOUT_MS); + } + + @Override + public void assertNoCallback() { + // TODO: better support this use case in TestableNetworkCallback + waitForIdle(); + assertNoCallback(0 /* timeout */); + } + + @Override + public T expectCallback(final KClass type, final HasNetwork n, + final long timeoutMs) { + final T callback = super.expectCallback(type, n, timeoutMs); + if (callback instanceof CallbackEntry.Losing) { + // TODO : move this to the specific test(s) needing this rather than here. + final CallbackEntry.Losing losing = (CallbackEntry.Losing) callback; + final int maxMsToLive = losing.getMaxMsToLive(); + String msg = String.format( + "Invalid linger time value %d, must be between %d and %d", + maxMsToLive, 0, mService.mLingerDelayMs); + assertTrue(msg, 0 <= maxMsToLive && maxMsToLive <= mService.mLingerDelayMs); + } + return callback; + } + } + + // Can't be part of TestNetworkCallback because "cannot be declared static; static methods can + // only be declared in a static or top level type". + static void assertNoCallbacks(TestNetworkCallback ... callbacks) { + for (TestNetworkCallback c : callbacks) { + c.assertNoCallback(); + } + } + + static void expectOnLost(TestNetworkAgentWrapper network, TestNetworkCallback ... callbacks) { + for (TestNetworkCallback c : callbacks) { + c.expectCallback(CallbackEntry.LOST, network); + } + } + + static void expectAvailableCallbacksUnvalidatedWithSpecifier(TestNetworkAgentWrapper network, + NetworkSpecifier specifier, TestNetworkCallback ... callbacks) { + for (TestNetworkCallback c : callbacks) { + c.expectCallback(CallbackEntry.AVAILABLE, network); + c.expectCapabilitiesThat(network, (nc) -> + !nc.hasCapability(NET_CAPABILITY_VALIDATED) + && Objects.equals(specifier, nc.getNetworkSpecifier())); + c.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, network); + c.expectCallback(CallbackEntry.BLOCKED_STATUS, network); + } + } + + @Test + public void testStateChangeNetworkCallbacks() throws Exception { + final TestNetworkCallback genericNetworkCallback = new TestNetworkCallback(); + final TestNetworkCallback wifiNetworkCallback = new TestNetworkCallback(); + final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback(); + final NetworkRequest genericRequest = new NetworkRequest.Builder() + .clearCapabilities().build(); + final NetworkRequest wifiRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).build(); + final NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + mCm.registerNetworkCallback(genericRequest, genericNetworkCallback); + mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback); + mCm.registerNetworkCallback(cellRequest, cellNetworkCallback); + + // Test unvalidated networks + ExpectedBroadcast b = registerConnectivityBroadcast(1); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(false); + genericNetworkCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + cellNetworkCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + b.expectBroadcast(); + assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback); + + // This should not trigger spurious onAvailable() callbacks, b/21762680. + mCellNetworkAgent.adjustScore(-1); + waitForIdle(); + assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + b = registerConnectivityBroadcast(2); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + genericNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + wifiNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + b.expectBroadcast(); + assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback); + + b = registerConnectivityBroadcast(2); + mWiFiNetworkAgent.disconnect(); + genericNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + wifiNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + cellNetworkCallback.assertNoCallback(); + b.expectBroadcast(); + assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback); + + b = registerConnectivityBroadcast(1); + mCellNetworkAgent.disconnect(); + genericNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + b.expectBroadcast(); + assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback); + + // Test validated networks + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + genericNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback); + + // This should not trigger spurious onAvailable() callbacks, b/21762680. + mCellNetworkAgent.adjustScore(-1); + waitForIdle(); + assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + genericNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + genericNetworkCallback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + genericNetworkCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + wifiNetworkCallback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent); + cellNetworkCallback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback); + + mWiFiNetworkAgent.disconnect(); + genericNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + wifiNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback); + + mCellNetworkAgent.disconnect(); + genericNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback); + } + + private void doNetworkCallbacksSanitizationTest(boolean sanitized) throws Exception { + final TestNetworkCallback callback = new TestNetworkCallback(); + final TestNetworkCallback defaultCallback = new TestNetworkCallback(); + final NetworkRequest wifiRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).build(); + mCm.registerNetworkCallback(wifiRequest, callback); + mCm.registerDefaultNetworkCallback(defaultCallback); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + + final LinkProperties newLp = new LinkProperties(); + final Uri capportUrl = Uri.parse("https://capport.example.com/api"); + final CaptivePortalData capportData = new CaptivePortalData.Builder() + .setCaptive(true).build(); + + final Uri expectedCapportUrl = sanitized ? null : capportUrl; + newLp.setCaptivePortalApiUrl(capportUrl); + mWiFiNetworkAgent.sendLinkProperties(newLp); + callback.expectLinkPropertiesThat(mWiFiNetworkAgent, lp -> + Objects.equals(expectedCapportUrl, lp.getCaptivePortalApiUrl())); + defaultCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, lp -> + Objects.equals(expectedCapportUrl, lp.getCaptivePortalApiUrl())); + + final CaptivePortalData expectedCapportData = sanitized ? null : capportData; + mWiFiNetworkAgent.notifyCapportApiDataChanged(capportData); + callback.expectLinkPropertiesThat(mWiFiNetworkAgent, lp -> + Objects.equals(expectedCapportData, lp.getCaptivePortalData())); + defaultCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, lp -> + Objects.equals(expectedCapportData, lp.getCaptivePortalData())); + + final LinkProperties lp = mCm.getLinkProperties(mWiFiNetworkAgent.getNetwork()); + assertEquals(expectedCapportUrl, lp.getCaptivePortalApiUrl()); + assertEquals(expectedCapportData, lp.getCaptivePortalData()); + } + + @Test + public void networkCallbacksSanitizationTest_Sanitize() throws Exception { + mServiceContext.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + PERMISSION_DENIED); + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_DENIED); + doNetworkCallbacksSanitizationTest(true /* sanitized */); + } + + @Test + public void networkCallbacksSanitizationTest_NoSanitize_NetworkStack() throws Exception { + mServiceContext.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + PERMISSION_GRANTED); + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_DENIED); + doNetworkCallbacksSanitizationTest(false /* sanitized */); + } + + @Test + public void networkCallbacksSanitizationTest_NoSanitize_Settings() throws Exception { + mServiceContext.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + PERMISSION_DENIED); + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + doNetworkCallbacksSanitizationTest(false /* sanitized */); + } + + @Test + public void testOwnerUidCannotChange() throws Exception { + final NetworkCapabilities ncTemplate = new NetworkCapabilities(); + final int originalOwnerUid = Process.myUid(); + ncTemplate.setOwnerUid(originalOwnerUid); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, new LinkProperties(), + ncTemplate); + mWiFiNetworkAgent.connect(false); + waitForIdle(); + + // Send ConnectivityService an update to the mWiFiNetworkAgent's capabilities that changes + // the owner UID and an unrelated capability. + NetworkCapabilities agentCapabilities = mWiFiNetworkAgent.getNetworkCapabilities(); + assertEquals(originalOwnerUid, agentCapabilities.getOwnerUid()); + agentCapabilities.setOwnerUid(42); + assertFalse(agentCapabilities.hasCapability(NET_CAPABILITY_NOT_CONGESTED)); + agentCapabilities.addCapability(NET_CAPABILITY_NOT_CONGESTED); + mWiFiNetworkAgent.setNetworkCapabilities(agentCapabilities, true); + waitForIdle(); + + // Owner UIDs are not visible without location permission. + setupLocationPermissions(Build.VERSION_CODES.Q, true, AppOpsManager.OPSTR_FINE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION); + + // Check that the capability change has been applied but the owner UID is not modified. + NetworkCapabilities nc = mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()); + assertEquals(originalOwnerUid, nc.getOwnerUid()); + assertTrue(nc.hasCapability(NET_CAPABILITY_NOT_CONGESTED)); + } + + @Test + public void testMultipleLingering() throws Exception { + // This test would be flaky with the default 120ms timer: that is short enough that + // lingered networks are torn down before assertions can be run. We don't want to mock the + // lingering timer to keep the WakeupMessage logic realistic: this has already proven useful + // in detecting races. + mService.mLingerDelayMs = 300; + + NetworkRequest request = new NetworkRequest.Builder() + .clearCapabilities().addCapability(NET_CAPABILITY_NOT_METERED) + .build(); + TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + TestNetworkCallback defaultCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(defaultCallback); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET); + + mCellNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mEthernetNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + + mCellNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + defaultCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + mWiFiNetworkAgent.connect(true); + // We get AVAILABLE on wifi when wifi connects and satisfies our unmetered request. + // We then get LOSING when wifi validates and cell is outscored. + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + // TODO: Investigate sending validated before losing. + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + mEthernetNetworkAgent.connect(true); + callback.expectAvailableCallbacksUnvalidated(mEthernetNetworkAgent); + // TODO: Investigate sending validated before losing. + callback.expectCallback(CallbackEntry.LOSING, mWiFiNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mEthernetNetworkAgent); + defaultCallback.expectAvailableDoubleValidatedCallbacks(mEthernetNetworkAgent); + assertEquals(mEthernetNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + mEthernetNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent); + defaultCallback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent); + defaultCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + for (int i = 0; i < 4; i++) { + TestNetworkAgentWrapper oldNetwork, newNetwork; + if (i % 2 == 0) { + mWiFiNetworkAgent.adjustScore(-15); + oldNetwork = mWiFiNetworkAgent; + newNetwork = mCellNetworkAgent; + } else { + mWiFiNetworkAgent.adjustScore(15); + oldNetwork = mCellNetworkAgent; + newNetwork = mWiFiNetworkAgent; + + } + callback.expectCallback(CallbackEntry.LOSING, oldNetwork); + // TODO: should we send an AVAILABLE callback to newNetwork, to indicate that it is no + // longer lingering? + defaultCallback.expectAvailableCallbacksValidated(newNetwork); + assertEquals(newNetwork.getNetwork(), mCm.getActiveNetwork()); + } + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + // Verify that if a network no longer satisfies a request, we send LOST and not LOSING, even + // if the network is still up. + mWiFiNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED); + // We expect a notification about the capabilities change, and nothing else. + defaultCallback.expectCapabilitiesWithout(NET_CAPABILITY_NOT_METERED, mWiFiNetworkAgent); + defaultCallback.assertNoCallback(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + // Wifi no longer satisfies our listen, which is for an unmetered network. + // But because its score is 55, it's still up (and the default network). + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + // Disconnect our test networks. + mWiFiNetworkAgent.disconnect(); + defaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + defaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + mCellNetworkAgent.disconnect(); + defaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + waitForIdle(); + assertEquals(null, mCm.getActiveNetwork()); + + mCm.unregisterNetworkCallback(callback); + waitForIdle(); + + // Check that a network is only lingered or torn down if it would not satisfy a request even + // if it validated. + request = new NetworkRequest.Builder().clearCapabilities().build(); + callback = new TestNetworkCallback(); + + mCm.registerNetworkCallback(request, callback); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(false); // Score: 10 + callback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + // Bring up wifi with a score of 20. + // Cell stays up because it would satisfy the default request if it validated. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); // Score: 20 + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + defaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + // Bring up wifi with a score of 70. + // Cell is lingered because it would not satisfy any request, even if it validated. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.adjustScore(50); + mWiFiNetworkAgent.connect(false); // Score: 70 + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + // Tear down wifi. + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + defaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + // Bring up wifi, then validate it. Previous versions would immediately tear down cell, but + // it's arguably correct to linger it, since it was the default network before it validated. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + // TODO: Investigate sending validated before losing. + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + defaultCallback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + defaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + mCellNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + defaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + waitForIdle(); + assertEquals(null, mCm.getActiveNetwork()); + + // If a network is lingering, and we add and remove a request from it, resume lingering. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + defaultCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + // TODO: Investigate sending validated before losing. + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + NetworkCallback noopCallback = new NetworkCallback(); + mCm.requestNetwork(cellRequest, noopCallback); + // TODO: should this cause an AVAILABLE callback, to indicate that the network is no longer + // lingering? + mCm.unregisterNetworkCallback(noopCallback); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + + // Similar to the above: lingering can start even after the lingered request is removed. + // Disconnect wifi and switch to cell. + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + defaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + defaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + // Cell is now the default network. Pin it with a cell-specific request. + noopCallback = new NetworkCallback(); // Can't reuse NetworkCallbacks. http://b/20701525 + mCm.requestNetwork(cellRequest, noopCallback); + + // Now connect wifi, and expect it to become the default network. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent); + defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + // The default request is lingering on cell, but nothing happens to cell, and we send no + // callbacks for it, because it's kept up by cellRequest. + callback.assertNoCallback(); + // Now unregister cellRequest and expect cell to start lingering. + mCm.unregisterNetworkCallback(noopCallback); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + + // Let linger run its course. + callback.assertNoCallback(); + final int lingerTimeoutMs = mService.mLingerDelayMs + mService.mLingerDelayMs / 4; + callback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent, lingerTimeoutMs); + + // Register a TRACK_DEFAULT request and check that it does not affect lingering. + TestNetworkCallback trackDefaultCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(trackDefaultCallback); + trackDefaultCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent); + mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET); + mEthernetNetworkAgent.connect(true); + callback.expectAvailableCallbacksUnvalidated(mEthernetNetworkAgent); + callback.expectCallback(CallbackEntry.LOSING, mWiFiNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mEthernetNetworkAgent); + trackDefaultCallback.expectAvailableDoubleValidatedCallbacks(mEthernetNetworkAgent); + defaultCallback.expectAvailableDoubleValidatedCallbacks(mEthernetNetworkAgent); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + // Let linger run its course. + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent, lingerTimeoutMs); + + // Clean up. + mEthernetNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent); + defaultCallback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent); + trackDefaultCallback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent); + + mCm.unregisterNetworkCallback(callback); + mCm.unregisterNetworkCallback(defaultCallback); + mCm.unregisterNetworkCallback(trackDefaultCallback); + } + + private void grantUsingBackgroundNetworksPermissionForUid(final int uid) throws Exception { + grantUsingBackgroundNetworksPermissionForUid(uid, mContext.getPackageName()); + } + + private void grantUsingBackgroundNetworksPermissionForUid( + final int uid, final String packageName) throws Exception { + when(mPackageManager.getPackageInfo( + eq(packageName), eq(GET_PERMISSIONS | MATCH_ANY_USER))) + .thenReturn(buildPackageInfo(true /* hasSystemPermission */, uid)); + mService.mPermissionMonitor.onPackageAdded(packageName, uid); + } + + @Test + public void testNetworkGoesIntoBackgroundAfterLinger() throws Exception { + setAlwaysOnNetworks(true); + grantUsingBackgroundNetworksPermissionForUid(Binder.getCallingUid()); + NetworkRequest request = new NetworkRequest.Builder() + .clearCapabilities() + .build(); + TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + TestNetworkCallback defaultCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(defaultCallback); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + + mCellNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + defaultCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + + // Wifi comes up and cell lingers. + mWiFiNetworkAgent.connect(true); + defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + + // File a request for cellular, then release it. + NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + NetworkCallback noopCallback = new NetworkCallback(); + mCm.requestNetwork(cellRequest, noopCallback); + mCm.unregisterNetworkCallback(noopCallback); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + + // Let linger run its course. + callback.assertNoCallback(); + final int lingerTimeoutMs = TEST_LINGER_DELAY_MS + TEST_LINGER_DELAY_MS / 4; + callback.expectCapabilitiesWithout(NET_CAPABILITY_FOREGROUND, mCellNetworkAgent, + lingerTimeoutMs); + + // Clean up. + mCm.unregisterNetworkCallback(defaultCallback); + mCm.unregisterNetworkCallback(callback); + } + + private NativeNetworkConfig nativeNetworkConfigPhysical(int netId, int permission) { + return new NativeNetworkConfig(netId, NativeNetworkType.PHYSICAL, permission, + /*secure=*/ false, VpnManager.TYPE_VPN_NONE); + } + + private NativeNetworkConfig nativeNetworkConfigVpn(int netId, boolean secure, int vpnType) { + return new NativeNetworkConfig(netId, NativeNetworkType.VIRTUAL, INetd.PERMISSION_NONE, + secure, vpnType); + } + + @Test + public void testNetworkAgentCallbacks() throws Exception { + // Keeps track of the order of events that happen in this test. + final LinkedBlockingQueue eventOrder = new LinkedBlockingQueue<>(); + + final NetworkRequest request = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).build(); + final TestNetworkCallback callback = new TestNetworkCallback(); + final AtomicReference wifiNetwork = new AtomicReference<>(); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + + // Expectations for state when various callbacks fire. These expectations run on the handler + // thread and not on the test thread because they need to prevent the handler thread from + // advancing while they examine state. + + // 1. When onCreated fires, netd has been told to create the network. + mWiFiNetworkAgent.setCreatedCallback(() -> { + eventOrder.offer("onNetworkCreated"); + wifiNetwork.set(mWiFiNetworkAgent.getNetwork()); + assertNotNull(wifiNetwork.get()); + try { + verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical( + wifiNetwork.get().getNetId(), INetd.PERMISSION_NONE)); + } catch (RemoteException impossible) { + fail(); + } + }); + + // 2. onNetworkUnwanted isn't precisely ordered with respect to any particular events. Just + // check that it is fired at some point after disconnect. + mWiFiNetworkAgent.setUnwantedCallback(() -> eventOrder.offer("onNetworkUnwanted")); + + // 3. While the teardown timer is running, connectivity APIs report the network is gone, but + // netd has not yet been told to destroy it. + final Runnable duringTeardown = () -> { + eventOrder.offer("timePasses"); + assertNull(mCm.getLinkProperties(wifiNetwork.get())); + try { + verify(mMockNetd, never()).networkDestroy(wifiNetwork.get().getNetId()); + } catch (RemoteException impossible) { + fail(); + } + }; + + // 4. After onNetworkDisconnected is called, connectivity APIs report the network is gone, + // and netd has been told to destroy it. + mWiFiNetworkAgent.setDisconnectedCallback(() -> { + eventOrder.offer("onNetworkDisconnected"); + assertNull(mCm.getLinkProperties(wifiNetwork.get())); + try { + verify(mMockNetd).networkDestroy(wifiNetwork.get().getNetId()); + } catch (RemoteException impossible) { + fail(); + } + }); + + // Connect a network, and file a request for it after it has come up, to ensure the nascent + // timer is cleared and the test does not have to wait for it. Filing the request after the + // network has come up is necessary because ConnectivityService does not appear to clear the + // nascent timer if the first request satisfied by the network was filed before the network + // connected. + // TODO: fix this bug, file the request before connecting, and remove the waitForIdle. + mWiFiNetworkAgent.connectWithoutInternet(); + waitForIdle(); + mCm.requestNetwork(request, callback); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + + // Set teardown delay and make sure CS has processed it. + mWiFiNetworkAgent.getNetworkAgent().setTeardownDelayMillis(300); + waitForIdle(); + + // Post the duringTeardown lambda to the handler so it fires while teardown is in progress. + // The delay must be long enough it will run after the unregisterNetworkCallback has torn + // down the network and started the teardown timer, and short enough that the lambda is + // scheduled to run before the teardown timer. + final Handler h = new Handler(mCsHandlerThread.getLooper()); + h.postDelayed(duringTeardown, 150); + + // Disconnect the network and check that events happened in the right order. + mCm.unregisterNetworkCallback(callback); + assertEquals("onNetworkCreated", eventOrder.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals("onNetworkUnwanted", eventOrder.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals("timePasses", eventOrder.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals("onNetworkDisconnected", eventOrder.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + + mCm.unregisterNetworkCallback(callback); + } + + @Test + public void testExplicitlySelected() throws Exception { + NetworkRequest request = new NetworkRequest.Builder() + .clearCapabilities().addCapability(NET_CAPABILITY_INTERNET) + .build(); + TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + // Bring up validated cell. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + + // Bring up unvalidated wifi with explicitlySelected=true. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.explicitlySelected(true, false); + mWiFiNetworkAgent.connect(false); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + + // Cell Remains the default. + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + // Lower wifi's score to below than cell, and check that it doesn't disconnect because + // it's explicitly selected. + mWiFiNetworkAgent.adjustScore(-40); + mWiFiNetworkAgent.adjustScore(40); + callback.assertNoCallback(); + + // If the user chooses yes on the "No Internet access, stay connected?" dialog, we switch to + // wifi even though it's unvalidated. + mCm.setAcceptUnvalidated(mWiFiNetworkAgent.getNetwork(), true, false); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + // Disconnect wifi, and then reconnect, again with explicitlySelected=true. + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.explicitlySelected(true, false); + mWiFiNetworkAgent.connect(false); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + + // If the user chooses no on the "No Internet access, stay connected?" dialog, we ask the + // network to disconnect. + mCm.setAcceptUnvalidated(mWiFiNetworkAgent.getNetwork(), false, false); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // Reconnect, again with explicitlySelected=true, but this time validate. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.explicitlySelected(true, false); + mWiFiNetworkAgent.connect(true); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + // BUG: the network will no longer linger, even though it's validated and outscored. + // TODO: fix this. + mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET); + mEthernetNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mEthernetNetworkAgent); + assertEquals(mEthernetNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + callback.assertNoCallback(); + + // Disconnect wifi, and then reconnect as if the user had selected "yes, don't ask again" + // (i.e., with explicitlySelected=true and acceptUnvalidated=true). Expect to switch to + // wifi immediately. + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.explicitlySelected(true, true); + mWiFiNetworkAgent.connect(false); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + callback.expectCallback(CallbackEntry.LOSING, mEthernetNetworkAgent); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + mEthernetNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent); + + // Disconnect and reconnect with explicitlySelected=false and acceptUnvalidated=true. + // Check that the network is not scored specially and that the device prefers cell data. + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.explicitlySelected(false, true); + mWiFiNetworkAgent.connect(false); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + // Clean up. + mWiFiNetworkAgent.disconnect(); + mCellNetworkAgent.disconnect(); + + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + callback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + } + + private void tryNetworkFactoryRequests(int capability) throws Exception { + // Verify NOT_RESTRICTED is set appropriately + final NetworkCapabilities nc = new NetworkRequest.Builder().addCapability(capability) + .build().networkCapabilities; + if (capability == NET_CAPABILITY_CBS || capability == NET_CAPABILITY_DUN + || capability == NET_CAPABILITY_EIMS || capability == NET_CAPABILITY_FOTA + || capability == NET_CAPABILITY_IA || capability == NET_CAPABILITY_IMS + || capability == NET_CAPABILITY_RCS || capability == NET_CAPABILITY_XCAP + || capability == NET_CAPABILITY_VSIM || capability == NET_CAPABILITY_BIP + || capability == NET_CAPABILITY_ENTERPRISE) { + assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + } else { + assertTrue(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + } + + NetworkCapabilities filter = new NetworkCapabilities(); + filter.addCapability(capability); + // Add NOT_VCN_MANAGED capability into filter unconditionally since some request will add + // NOT_VCN_MANAGED automatically but not for NetworkCapabilities, + // see {@code NetworkCapabilities#deduceNotVcnManagedCapability} for more details. + filter.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED); + final HandlerThread handlerThread = new HandlerThread("testNetworkFactoryRequests"); + handlerThread.start(); + final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(), + mServiceContext, "testFactory", filter, mCsHandlerThread); + testFactory.setScoreFilter(40); + ConditionVariable cv = testFactory.getNetworkStartedCV(); + testFactory.register(); + testFactory.expectRequestAdd(); + testFactory.assertRequestCountEquals(1); + int expectedRequestCount = 1; + NetworkCallback networkCallback = null; + // For non-INTERNET capabilities we cannot rely on the default request being present, so + // add one. + if (capability != NET_CAPABILITY_INTERNET) { + assertFalse(testFactory.getMyStartRequested()); + NetworkRequest request = new NetworkRequest.Builder().addCapability(capability).build(); + networkCallback = new NetworkCallback(); + mCm.requestNetwork(request, networkCallback); + expectedRequestCount++; + testFactory.expectRequestAdd(); + } + waitFor(cv); + testFactory.assertRequestCountEquals(expectedRequestCount); + assertTrue(testFactory.getMyStartRequested()); + + // Now bring in a higher scored network. + TestNetworkAgentWrapper testAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + // Rather than create a validated network which complicates things by registering it's + // own NetworkRequest during startup, just bump up the score to cancel out the + // unvalidated penalty. + testAgent.adjustScore(40); + cv = testFactory.getNetworkStoppedCV(); + + // When testAgent connects, ConnectivityService will re-send us all current requests with + // the new score. There are expectedRequestCount such requests, and we must wait for all of + // them. + testAgent.connect(false); + testAgent.addCapability(capability); + waitFor(cv); + testFactory.expectRequestAdds(expectedRequestCount); + testFactory.assertRequestCountEquals(expectedRequestCount); + assertFalse(testFactory.getMyStartRequested()); + + // Bring in a bunch of requests. + assertEquals(expectedRequestCount, testFactory.getMyRequestCount()); + ConnectivityManager.NetworkCallback[] networkCallbacks = + new ConnectivityManager.NetworkCallback[10]; + for (int i = 0; i< networkCallbacks.length; i++) { + networkCallbacks[i] = new ConnectivityManager.NetworkCallback(); + NetworkRequest.Builder builder = new NetworkRequest.Builder(); + builder.addCapability(capability); + mCm.requestNetwork(builder.build(), networkCallbacks[i]); + } + testFactory.expectRequestAdds(10); + testFactory.assertRequestCountEquals(10 + expectedRequestCount); + assertFalse(testFactory.getMyStartRequested()); + + // Remove the requests. + for (int i = 0; i < networkCallbacks.length; i++) { + mCm.unregisterNetworkCallback(networkCallbacks[i]); + } + testFactory.expectRequestRemoves(10); + testFactory.assertRequestCountEquals(expectedRequestCount); + assertFalse(testFactory.getMyStartRequested()); + + // Drop the higher scored network. + cv = testFactory.getNetworkStartedCV(); + testAgent.disconnect(); + waitFor(cv); + testFactory.expectRequestAdds(expectedRequestCount); + testFactory.assertRequestCountEquals(expectedRequestCount); + assertEquals(expectedRequestCount, testFactory.getMyRequestCount()); + assertTrue(testFactory.getMyStartRequested()); + + testFactory.terminate(); + if (networkCallback != null) mCm.unregisterNetworkCallback(networkCallback); + handlerThread.quit(); + } + + @Test + public void testNetworkFactoryRequests() throws Exception { + tryNetworkFactoryRequests(NET_CAPABILITY_MMS); + tryNetworkFactoryRequests(NET_CAPABILITY_SUPL); + tryNetworkFactoryRequests(NET_CAPABILITY_DUN); + tryNetworkFactoryRequests(NET_CAPABILITY_FOTA); + tryNetworkFactoryRequests(NET_CAPABILITY_IMS); + tryNetworkFactoryRequests(NET_CAPABILITY_CBS); + tryNetworkFactoryRequests(NET_CAPABILITY_WIFI_P2P); + tryNetworkFactoryRequests(NET_CAPABILITY_IA); + tryNetworkFactoryRequests(NET_CAPABILITY_RCS); + tryNetworkFactoryRequests(NET_CAPABILITY_XCAP); + tryNetworkFactoryRequests(NET_CAPABILITY_ENTERPRISE); + tryNetworkFactoryRequests(NET_CAPABILITY_EIMS); + tryNetworkFactoryRequests(NET_CAPABILITY_NOT_METERED); + tryNetworkFactoryRequests(NET_CAPABILITY_INTERNET); + tryNetworkFactoryRequests(NET_CAPABILITY_TRUSTED); + tryNetworkFactoryRequests(NET_CAPABILITY_NOT_VPN); + tryNetworkFactoryRequests(NET_CAPABILITY_VSIM); + tryNetworkFactoryRequests(NET_CAPABILITY_BIP); + // Skipping VALIDATED and CAPTIVE_PORTAL as they're disallowed. + } + + @Test + public void testNetworkFactoryUnregister() throws Exception { + final NetworkCapabilities filter = new NetworkCapabilities(); + filter.clearAll(); + + final HandlerThread handlerThread = new HandlerThread("testNetworkFactoryRequests"); + handlerThread.start(); + + // Checks that calling setScoreFilter on a NetworkFactory immediately before closing it + // does not crash. + for (int i = 0; i < 100; i++) { + final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(), + mServiceContext, "testFactory", filter, mCsHandlerThread); + // Register the factory and don't be surprised when the default request arrives. + testFactory.register(); + testFactory.expectRequestAdd(); + + testFactory.setScoreFilter(42); + testFactory.terminate(); + + if (i % 2 == 0) { + try { + testFactory.register(); + fail("Re-registering terminated NetworkFactory should throw"); + } catch (IllegalStateException expected) { + } + } + } + handlerThread.quit(); + } + + @Test + public void testNoMutableNetworkRequests() throws Exception { + final PendingIntent pendingIntent = PendingIntent.getBroadcast( + mContext, 0 /* requestCode */, new Intent("a"), FLAG_IMMUTABLE); + NetworkRequest request1 = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_VALIDATED) + .build(); + NetworkRequest request2 = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL) + .build(); + + Class expected = IllegalArgumentException.class; + assertThrows(expected, () -> mCm.requestNetwork(request1, new NetworkCallback())); + assertThrows(expected, () -> mCm.requestNetwork(request1, pendingIntent)); + assertThrows(expected, () -> mCm.requestNetwork(request2, new NetworkCallback())); + assertThrows(expected, () -> mCm.requestNetwork(request2, pendingIntent)); + } + + @Test + public void testMMSonWiFi() throws Exception { + // Test bringing up cellular without MMS NetworkRequest gets reaped + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.addCapability(NET_CAPABILITY_MMS); + mCellNetworkAgent.connectWithoutInternet(); + mCellNetworkAgent.expectDisconnected(); + waitForIdle(); + assertEmpty(mCm.getAllNetworks()); + verifyNoNetwork(); + + // Test bringing up validated WiFi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + final ExpectedBroadcast b = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED); + mWiFiNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + + // Register MMS NetworkRequest + NetworkRequest.Builder builder = new NetworkRequest.Builder(); + builder.addCapability(NetworkCapabilities.NET_CAPABILITY_MMS); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + mCm.requestNetwork(builder.build(), networkCallback); + + // Test bringing up unvalidated cellular with MMS + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.addCapability(NET_CAPABILITY_MMS); + mCellNetworkAgent.connectWithoutInternet(); + networkCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + verifyActiveNetwork(TRANSPORT_WIFI); + + // Test releasing NetworkRequest disconnects cellular with MMS + mCm.unregisterNetworkCallback(networkCallback); + mCellNetworkAgent.expectDisconnected(); + verifyActiveNetwork(TRANSPORT_WIFI); + } + + @Test + public void testMMSonCell() throws Exception { + // Test bringing up cellular without MMS + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + ExpectedBroadcast b = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTED); + mCellNetworkAgent.connect(false); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + + // Register MMS NetworkRequest + NetworkRequest.Builder builder = new NetworkRequest.Builder(); + builder.addCapability(NetworkCapabilities.NET_CAPABILITY_MMS); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + mCm.requestNetwork(builder.build(), networkCallback); + + // Test bringing up MMS cellular network + TestNetworkAgentWrapper + mmsNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mmsNetworkAgent.addCapability(NET_CAPABILITY_MMS); + mmsNetworkAgent.connectWithoutInternet(); + networkCallback.expectAvailableCallbacksUnvalidated(mmsNetworkAgent); + verifyActiveNetwork(TRANSPORT_CELLULAR); + + // Test releasing MMS NetworkRequest does not disconnect main cellular NetworkAgent + mCm.unregisterNetworkCallback(networkCallback); + mmsNetworkAgent.expectDisconnected(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + } + + @Test + public void testPartialConnectivity() throws Exception { + // Register network callback. + NetworkRequest request = new NetworkRequest.Builder() + .clearCapabilities().addCapability(NET_CAPABILITY_INTERNET) + .build(); + TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + // Bring up validated mobile data. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + + // Bring up wifi with partial connectivity. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connectWithPartialConnectivity(); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_PARTIAL_CONNECTIVITY, mWiFiNetworkAgent); + + // Mobile data should be the default network. + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + callback.assertNoCallback(); + + // With HTTPS probe disabled, NetworkMonitor should pass the network validation with http + // probe. + mWiFiNetworkAgent.setNetworkPartialValid(false /* isStrictMode */); + // If the user chooses yes to use this partial connectivity wifi, switch the default + // network to wifi and check if wifi becomes valid or not. + mCm.setAcceptPartialConnectivity(mWiFiNetworkAgent.getNetwork(), true /* accept */, + false /* always */); + // If user accepts partial connectivity network, + // NetworkMonitor#setAcceptPartialConnectivity() should be called too. + waitForIdle(); + verify(mWiFiNetworkAgent.mNetworkMonitor, times(1)).setAcceptPartialConnectivity(); + + // Need a trigger point to let NetworkMonitor tell ConnectivityService that network is + // validated. + mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + NetworkCapabilities nc = callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, + mWiFiNetworkAgent); + assertTrue(nc.hasCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY)); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + // Disconnect and reconnect wifi with partial connectivity again. + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connectWithPartialConnectivity(); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_PARTIAL_CONNECTIVITY, mWiFiNetworkAgent); + + // Mobile data should be the default network. + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + // If the user chooses no, disconnect wifi immediately. + mCm.setAcceptPartialConnectivity(mWiFiNetworkAgent.getNetwork(), false/* accept */, + false /* always */); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // If user accepted partial connectivity before, and device reconnects to that network + // again, but now the network has full connectivity. The network shouldn't contain + // NET_CAPABILITY_PARTIAL_CONNECTIVITY. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + // acceptUnvalidated is also used as setting for accepting partial networks. + mWiFiNetworkAgent.explicitlySelected(true /* explicitlySelected */, + true /* acceptUnvalidated */); + mWiFiNetworkAgent.connect(true); + + // If user accepted partial connectivity network before, + // NetworkMonitor#setAcceptPartialConnectivity() will be called in + // ConnectivityService#updateNetworkInfo(). + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + verify(mWiFiNetworkAgent.mNetworkMonitor, times(1)).setAcceptPartialConnectivity(); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + nc = callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + assertFalse(nc.hasCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY)); + + // Wifi should be the default network. + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // The user accepted partial connectivity and selected "don't ask again". Now the user + // reconnects to the partial connectivity network. Switch to wifi as soon as partial + // connectivity is detected. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.explicitlySelected(true /* explicitlySelected */, + true /* acceptUnvalidated */); + mWiFiNetworkAgent.connectWithPartialConnectivity(); + // If user accepted partial connectivity network before, + // NetworkMonitor#setAcceptPartialConnectivity() will be called in + // ConnectivityService#updateNetworkInfo(). + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + verify(mWiFiNetworkAgent.mNetworkMonitor, times(1)).setAcceptPartialConnectivity(); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + callback.expectCapabilitiesWith(NET_CAPABILITY_PARTIAL_CONNECTIVITY, mWiFiNetworkAgent); + mWiFiNetworkAgent.setNetworkValid(false /* isStrictMode */); + + // Need a trigger point to let NetworkMonitor tell ConnectivityService that network is + // validated. + mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true); + callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // If the user accepted partial connectivity, and the device auto-reconnects to the partial + // connectivity network, it should contain both PARTIAL_CONNECTIVITY and VALIDATED. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.explicitlySelected(false /* explicitlySelected */, + true /* acceptUnvalidated */); + + // NetworkMonitor will immediately (once the HTTPS probe fails...) report the network as + // valid, because ConnectivityService calls setAcceptPartialConnectivity before it calls + // notifyNetworkConnected. + mWiFiNetworkAgent.connectWithPartialValidConnectivity(false /* isStrictMode */); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + verify(mWiFiNetworkAgent.mNetworkMonitor, times(1)).setAcceptPartialConnectivity(); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + callback.expectCapabilitiesWith( + NET_CAPABILITY_PARTIAL_CONNECTIVITY | NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + } + + @Test + public void testCaptivePortalOnPartialConnectivity() throws Exception { + final TestNetworkCallback captivePortalCallback = new TestNetworkCallback(); + final NetworkRequest captivePortalRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build(); + mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback); + + final TestNetworkCallback validatedCallback = new TestNetworkCallback(); + final NetworkRequest validatedRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_VALIDATED).build(); + mCm.registerNetworkCallback(validatedRequest, validatedCallback); + + // Bring up a network with a captive portal. + // Expect onAvailable callback of listen for NET_CAPABILITY_CAPTIVE_PORTAL. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + String redirectUrl = "http://android.com/path"; + mWiFiNetworkAgent.connectWithCaptivePortal(redirectUrl, false /* isStrictMode */); + captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + assertEquals(mWiFiNetworkAgent.waitForRedirectUrl(), redirectUrl); + + // Check that startCaptivePortalApp sends the expected command to NetworkMonitor. + mCm.startCaptivePortalApp(mWiFiNetworkAgent.getNetwork()); + verify(mWiFiNetworkAgent.mNetworkMonitor, timeout(TIMEOUT_MS).times(1)) + .launchCaptivePortalApp(); + + // Report that the captive portal is dismissed with partial connectivity, and check that + // callbacks are fired. + mWiFiNetworkAgent.setNetworkPartial(); + mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true); + waitForIdle(); + captivePortalCallback.expectCapabilitiesWith(NET_CAPABILITY_PARTIAL_CONNECTIVITY, + mWiFiNetworkAgent); + + // Report partial connectivity is accepted. + mWiFiNetworkAgent.setNetworkPartialValid(false /* isStrictMode */); + mCm.setAcceptPartialConnectivity(mWiFiNetworkAgent.getNetwork(), true /* accept */, + false /* always */); + waitForIdle(); + mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true); + captivePortalCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + validatedCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent); + NetworkCapabilities nc = + validatedCallback.expectCapabilitiesWith(NET_CAPABILITY_PARTIAL_CONNECTIVITY, + mWiFiNetworkAgent); + + mCm.unregisterNetworkCallback(captivePortalCallback); + mCm.unregisterNetworkCallback(validatedCallback); + } + + @Test + public void testCaptivePortal() throws Exception { + final TestNetworkCallback captivePortalCallback = new TestNetworkCallback(); + final NetworkRequest captivePortalRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build(); + mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback); + + final TestNetworkCallback validatedCallback = new TestNetworkCallback(); + final NetworkRequest validatedRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_VALIDATED).build(); + mCm.registerNetworkCallback(validatedRequest, validatedCallback); + + // Bring up a network with a captive portal. + // Expect onAvailable callback of listen for NET_CAPABILITY_CAPTIVE_PORTAL. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + String firstRedirectUrl = "http://example.com/firstPath"; + mWiFiNetworkAgent.connectWithCaptivePortal(firstRedirectUrl, false /* isStrictMode */); + captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + assertEquals(mWiFiNetworkAgent.waitForRedirectUrl(), firstRedirectUrl); + + // Take down network. + // Expect onLost callback. + mWiFiNetworkAgent.disconnect(); + captivePortalCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // Bring up a network with a captive portal. + // Expect onAvailable callback of listen for NET_CAPABILITY_CAPTIVE_PORTAL. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + String secondRedirectUrl = "http://example.com/secondPath"; + mWiFiNetworkAgent.connectWithCaptivePortal(secondRedirectUrl, false /* isStrictMode */); + captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + assertEquals(mWiFiNetworkAgent.waitForRedirectUrl(), secondRedirectUrl); + + // Make captive portal disappear then revalidate. + // Expect onLost callback because network no longer provides NET_CAPABILITY_CAPTIVE_PORTAL. + mWiFiNetworkAgent.setNetworkValid(false /* isStrictMode */); + mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true); + captivePortalCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // Expect NET_CAPABILITY_VALIDATED onAvailable callback. + validatedCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + + // Break network connectivity. + // Expect NET_CAPABILITY_VALIDATED onLost callback. + mWiFiNetworkAgent.setNetworkInvalid(false /* isStrictMode */); + mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), false); + validatedCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + } + + @Test + public void testCaptivePortalApp() throws Exception { + final TestNetworkCallback captivePortalCallback = new TestNetworkCallback(); + final NetworkRequest captivePortalRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build(); + mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback); + + final TestNetworkCallback validatedCallback = new TestNetworkCallback(); + final NetworkRequest validatedRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_VALIDATED).build(); + mCm.registerNetworkCallback(validatedRequest, validatedCallback); + + // Bring up wifi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + validatedCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + Network wifiNetwork = mWiFiNetworkAgent.getNetwork(); + + // Check that calling startCaptivePortalApp does nothing. + final int fastTimeoutMs = 100; + mCm.startCaptivePortalApp(wifiNetwork); + waitForIdle(); + verify(mWiFiNetworkAgent.mNetworkMonitor, never()).launchCaptivePortalApp(); + mServiceContext.expectNoStartActivityIntent(fastTimeoutMs); + + // Turn into a captive portal. + mWiFiNetworkAgent.setNetworkPortal("http://example.com", false /* isStrictMode */); + mCm.reportNetworkConnectivity(wifiNetwork, false); + captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + validatedCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // Check that startCaptivePortalApp sends the expected command to NetworkMonitor. + mCm.startCaptivePortalApp(wifiNetwork); + waitForIdle(); + verify(mWiFiNetworkAgent.mNetworkMonitor).launchCaptivePortalApp(); + + // NetworkMonitor uses startCaptivePortal(Network, Bundle) (startCaptivePortalAppInternal) + final Bundle testBundle = new Bundle(); + final String testKey = "testkey"; + final String testValue = "testvalue"; + testBundle.putString(testKey, testValue); + mServiceContext.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + PERMISSION_GRANTED); + mCm.startCaptivePortalApp(wifiNetwork, testBundle); + final Intent signInIntent = mServiceContext.expectStartActivityIntent(TIMEOUT_MS); + assertEquals(ACTION_CAPTIVE_PORTAL_SIGN_IN, signInIntent.getAction()); + assertEquals(testValue, signInIntent.getStringExtra(testKey)); + + // Report that the captive portal is dismissed, and check that callbacks are fired + mWiFiNetworkAgent.setNetworkValid(false /* isStrictMode */); + mWiFiNetworkAgent.mNetworkMonitor.forceReevaluation(Process.myUid()); + validatedCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent); + captivePortalCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + mCm.unregisterNetworkCallback(validatedCallback); + mCm.unregisterNetworkCallback(captivePortalCallback); + } + + @Test + public void testAvoidOrIgnoreCaptivePortals() throws Exception { + final TestNetworkCallback captivePortalCallback = new TestNetworkCallback(); + final NetworkRequest captivePortalRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build(); + mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback); + + final TestNetworkCallback validatedCallback = new TestNetworkCallback(); + final NetworkRequest validatedRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_VALIDATED).build(); + mCm.registerNetworkCallback(validatedRequest, validatedCallback); + + setCaptivePortalMode(ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_AVOID); + // Bring up a network with a captive portal. + // Expect it to fail to connect and not result in any callbacks. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + String firstRedirectUrl = "http://example.com/firstPath"; + + mWiFiNetworkAgent.connectWithCaptivePortal(firstRedirectUrl, false /* isStrictMode */); + mWiFiNetworkAgent.expectDisconnected(); + mWiFiNetworkAgent.expectPreventReconnectReceived(); + + assertNoCallbacks(captivePortalCallback, validatedCallback); + } + + @Test + public void testCaptivePortalApi() throws Exception { + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + + final TestNetworkCallback captivePortalCallback = new TestNetworkCallback(); + final NetworkRequest captivePortalRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build(); + mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + final String redirectUrl = "http://example.com/firstPath"; + + mWiFiNetworkAgent.connectWithCaptivePortal(redirectUrl, false /* isStrictMode */); + captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + + final CaptivePortalData testData = new CaptivePortalData.Builder() + .setUserPortalUrl(Uri.parse(redirectUrl)) + .setBytesRemaining(12345L) + .build(); + + mWiFiNetworkAgent.notifyCapportApiDataChanged(testData); + + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> testData.equals(lp.getCaptivePortalData())); + + final LinkProperties newLps = new LinkProperties(); + newLps.setMtu(1234); + mWiFiNetworkAgent.sendLinkProperties(newLps); + // CaptivePortalData is not lost and unchanged when LPs are received from the NetworkAgent + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> testData.equals(lp.getCaptivePortalData()) && lp.getMtu() == 1234); + } + + private TestNetworkCallback setupNetworkCallbackAndConnectToWifi() throws Exception { + // Grant NETWORK_SETTINGS permission to be able to receive LinkProperties change callbacks + // with sensitive (captive portal) data + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + + final TestNetworkCallback captivePortalCallback = new TestNetworkCallback(); + final NetworkRequest captivePortalRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build(); + mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + + mWiFiNetworkAgent.connectWithCaptivePortal(TEST_REDIRECT_URL, false /* isStrictMode */); + captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + return captivePortalCallback; + } + + private class CaptivePortalTestData { + CaptivePortalTestData(CaptivePortalData naPasspointData, CaptivePortalData capportData, + CaptivePortalData naOtherData, CaptivePortalData expectedMergedPasspointData, + CaptivePortalData expectedMergedOtherData) { + mNaPasspointData = naPasspointData; + mCapportData = capportData; + mNaOtherData = naOtherData; + mExpectedMergedPasspointData = expectedMergedPasspointData; + mExpectedMergedOtherData = expectedMergedOtherData; + } + + public final CaptivePortalData mNaPasspointData; + public final CaptivePortalData mCapportData; + public final CaptivePortalData mNaOtherData; + public final CaptivePortalData mExpectedMergedPasspointData; + public final CaptivePortalData mExpectedMergedOtherData; + + } + + private CaptivePortalTestData setupCaptivePortalData() { + final CaptivePortalData capportData = new CaptivePortalData.Builder() + .setUserPortalUrl(Uri.parse(TEST_REDIRECT_URL)) + .setVenueInfoUrl(Uri.parse(TEST_VENUE_URL_CAPPORT)) + .setUserPortalUrl(Uri.parse(TEST_USER_PORTAL_API_URL_CAPPORT)) + .setExpiryTime(1000000L) + .setBytesRemaining(12345L) + .build(); + + final CaptivePortalData naPasspointData = new CaptivePortalData.Builder() + .setBytesRemaining(80802L) + .setVenueInfoUrl(Uri.parse(TEST_VENUE_URL_NA_PASSPOINT), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) + .setUserPortalUrl(Uri.parse(TEST_TERMS_AND_CONDITIONS_URL_NA_PASSPOINT), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) + .setVenueFriendlyName(TEST_FRIENDLY_NAME).build(); + + final CaptivePortalData naOtherData = new CaptivePortalData.Builder() + .setBytesRemaining(80802L) + .setVenueInfoUrl(Uri.parse(TEST_VENUE_URL_NA_OTHER), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER) + .setUserPortalUrl(Uri.parse(TEST_TERMS_AND_CONDITIONS_URL_NA_OTHER), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER) + .setVenueFriendlyName(TEST_FRIENDLY_NAME).build(); + + final CaptivePortalData expectedMergedPasspointData = new CaptivePortalData.Builder() + .setUserPortalUrl(Uri.parse(TEST_REDIRECT_URL)) + .setBytesRemaining(12345L) + .setExpiryTime(1000000L) + .setVenueInfoUrl(Uri.parse(TEST_VENUE_URL_NA_PASSPOINT), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) + .setUserPortalUrl(Uri.parse(TEST_TERMS_AND_CONDITIONS_URL_NA_PASSPOINT), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) + .setVenueFriendlyName(TEST_FRIENDLY_NAME).build(); + + final CaptivePortalData expectedMergedOtherData = new CaptivePortalData.Builder() + .setUserPortalUrl(Uri.parse(TEST_REDIRECT_URL)) + .setBytesRemaining(12345L) + .setExpiryTime(1000000L) + .setVenueInfoUrl(Uri.parse(TEST_VENUE_URL_CAPPORT)) + .setUserPortalUrl(Uri.parse(TEST_USER_PORTAL_API_URL_CAPPORT)) + .setVenueFriendlyName(TEST_FRIENDLY_NAME).build(); + return new CaptivePortalTestData(naPasspointData, capportData, naOtherData, + expectedMergedPasspointData, expectedMergedOtherData); + } + + @Test + public void testMergeCaptivePortalApiWithFriendlyNameAndVenueUrl() throws Exception { + final TestNetworkCallback captivePortalCallback = setupNetworkCallbackAndConnectToWifi(); + final CaptivePortalTestData captivePortalTestData = setupCaptivePortalData(); + + // Baseline capport data + mWiFiNetworkAgent.notifyCapportApiDataChanged(captivePortalTestData.mCapportData); + + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> captivePortalTestData.mCapportData.equals(lp.getCaptivePortalData())); + + // Venue URL, T&C URL and friendly name from Network agent with Passpoint source, confirm + // that API data gets precedence on the bytes remaining. + final LinkProperties linkProperties = new LinkProperties(); + linkProperties.setCaptivePortalData(captivePortalTestData.mNaPasspointData); + mWiFiNetworkAgent.sendLinkProperties(linkProperties); + + // Make sure that the capport data is merged + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> captivePortalTestData.mExpectedMergedPasspointData + .equals(lp.getCaptivePortalData())); + + // Now send this information from non-Passpoint source, confirm that Capport data takes + // precedence + linkProperties.setCaptivePortalData(captivePortalTestData.mNaOtherData); + mWiFiNetworkAgent.sendLinkProperties(linkProperties); + + // Make sure that the capport data is merged + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> captivePortalTestData.mExpectedMergedOtherData + .equals(lp.getCaptivePortalData())); + + // Create a new LP with no Network agent capport data + final LinkProperties newLps = new LinkProperties(); + newLps.setMtu(1234); + mWiFiNetworkAgent.sendLinkProperties(newLps); + // CaptivePortalData is not lost and has the original values when LPs are received from the + // NetworkAgent + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> captivePortalTestData.mCapportData.equals(lp.getCaptivePortalData()) + && lp.getMtu() == 1234); + + // Now send capport data only from the Network agent + mWiFiNetworkAgent.notifyCapportApiDataChanged(null); + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> lp.getCaptivePortalData() == null); + + newLps.setCaptivePortalData(captivePortalTestData.mNaPasspointData); + mWiFiNetworkAgent.sendLinkProperties(newLps); + + // Make sure that only the network agent capport data is available + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> captivePortalTestData.mNaPasspointData.equals(lp.getCaptivePortalData())); + } + + @Test + public void testMergeCaptivePortalDataFromNetworkAgentFirstThenCapport() throws Exception { + final TestNetworkCallback captivePortalCallback = setupNetworkCallbackAndConnectToWifi(); + final CaptivePortalTestData captivePortalTestData = setupCaptivePortalData(); + + // Venue URL and friendly name from Network agent, confirm that API data gets precedence + // on the bytes remaining. + final LinkProperties linkProperties = new LinkProperties(); + linkProperties.setCaptivePortalData(captivePortalTestData.mNaPasspointData); + mWiFiNetworkAgent.sendLinkProperties(linkProperties); + + // Make sure that the data is saved correctly + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> captivePortalTestData.mNaPasspointData.equals(lp.getCaptivePortalData())); + + // Expected merged data: Network agent data is preferred, and values that are not used by + // it are merged from capport data + mWiFiNetworkAgent.notifyCapportApiDataChanged(captivePortalTestData.mCapportData); + + // Make sure that the Capport data is merged correctly + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> captivePortalTestData.mExpectedMergedPasspointData.equals( + lp.getCaptivePortalData())); + + // Now set the naData to null + linkProperties.setCaptivePortalData(null); + mWiFiNetworkAgent.sendLinkProperties(linkProperties); + + // Make sure that the Capport data is retained correctly + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> captivePortalTestData.mCapportData.equals(lp.getCaptivePortalData())); + } + + @Test + public void testMergeCaptivePortalDataFromNetworkAgentOtherSourceFirstThenCapport() + throws Exception { + final TestNetworkCallback captivePortalCallback = setupNetworkCallbackAndConnectToWifi(); + final CaptivePortalTestData captivePortalTestData = setupCaptivePortalData(); + + // Venue URL and friendly name from Network agent, confirm that API data gets precedence + // on the bytes remaining. + final LinkProperties linkProperties = new LinkProperties(); + linkProperties.setCaptivePortalData(captivePortalTestData.mNaOtherData); + mWiFiNetworkAgent.sendLinkProperties(linkProperties); + + // Make sure that the data is saved correctly + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> captivePortalTestData.mNaOtherData.equals(lp.getCaptivePortalData())); + + // Expected merged data: Network agent data is preferred, and values that are not used by + // it are merged from capport data + mWiFiNetworkAgent.notifyCapportApiDataChanged(captivePortalTestData.mCapportData); + + // Make sure that the Capport data is merged correctly + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> captivePortalTestData.mExpectedMergedOtherData.equals( + lp.getCaptivePortalData())); + } + + private NetworkRequest.Builder newWifiRequestBuilder() { + return new NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI); + } + + /** + * Verify request matching behavior with network specifiers. + * + * This test does not check updating the specifier on a live network because the specifier is + * immutable and this triggers a WTF in + * {@link ConnectivityService#mixInCapabilities(NetworkAgentInfo, NetworkCapabilities)}. + */ + @Test + public void testNetworkSpecifier() throws Exception { + // A NetworkSpecifier subclass that matches all networks but must not be visible to apps. + class ConfidentialMatchAllNetworkSpecifier extends NetworkSpecifier implements + Parcelable { + @Override + public boolean canBeSatisfiedBy(NetworkSpecifier other) { + return true; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) {} + + @Override + public NetworkSpecifier redact() { + return null; + } + } + + // A network specifier that matches either another LocalNetworkSpecifier with the same + // string or a ConfidentialMatchAllNetworkSpecifier, and can be passed to apps as is. + class LocalStringNetworkSpecifier extends NetworkSpecifier implements Parcelable { + private String mString; + + LocalStringNetworkSpecifier(String string) { + mString = string; + } + + @Override + public boolean canBeSatisfiedBy(NetworkSpecifier other) { + if (other instanceof LocalStringNetworkSpecifier) { + return TextUtils.equals(mString, + ((LocalStringNetworkSpecifier) other).mString); + } + if (other instanceof ConfidentialMatchAllNetworkSpecifier) return true; + return false; + } + + @Override + public int describeContents() { + return 0; + } + @Override + public void writeToParcel(Parcel dest, int flags) {} + } + + + NetworkRequest rEmpty1 = newWifiRequestBuilder().build(); + NetworkRequest rEmpty2 = newWifiRequestBuilder().setNetworkSpecifier((String) null).build(); + NetworkRequest rEmpty3 = newWifiRequestBuilder().setNetworkSpecifier("").build(); + NetworkRequest rEmpty4 = newWifiRequestBuilder().setNetworkSpecifier( + (NetworkSpecifier) null).build(); + NetworkRequest rFoo = newWifiRequestBuilder().setNetworkSpecifier( + new LocalStringNetworkSpecifier("foo")).build(); + NetworkRequest rBar = newWifiRequestBuilder().setNetworkSpecifier( + new LocalStringNetworkSpecifier("bar")).build(); + + TestNetworkCallback cEmpty1 = new TestNetworkCallback(); + TestNetworkCallback cEmpty2 = new TestNetworkCallback(); + TestNetworkCallback cEmpty3 = new TestNetworkCallback(); + TestNetworkCallback cEmpty4 = new TestNetworkCallback(); + TestNetworkCallback cFoo = new TestNetworkCallback(); + TestNetworkCallback cBar = new TestNetworkCallback(); + TestNetworkCallback[] emptyCallbacks = new TestNetworkCallback[] { + cEmpty1, cEmpty2, cEmpty3, cEmpty4 }; + + mCm.registerNetworkCallback(rEmpty1, cEmpty1); + mCm.registerNetworkCallback(rEmpty2, cEmpty2); + mCm.registerNetworkCallback(rEmpty3, cEmpty3); + mCm.registerNetworkCallback(rEmpty4, cEmpty4); + mCm.registerNetworkCallback(rFoo, cFoo); + mCm.registerNetworkCallback(rBar, cBar); + + LocalStringNetworkSpecifier nsFoo = new LocalStringNetworkSpecifier("foo"); + LocalStringNetworkSpecifier nsBar = new LocalStringNetworkSpecifier("bar"); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + expectAvailableCallbacksUnvalidatedWithSpecifier(mWiFiNetworkAgent, null /* specifier */, + cEmpty1, cEmpty2, cEmpty3, cEmpty4); + assertNoCallbacks(cFoo, cBar); + + mWiFiNetworkAgent.disconnect(); + expectOnLost(mWiFiNetworkAgent, cEmpty1, cEmpty2, cEmpty3, cEmpty4); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.setNetworkSpecifier(nsFoo); + mWiFiNetworkAgent.connect(false); + expectAvailableCallbacksUnvalidatedWithSpecifier(mWiFiNetworkAgent, nsFoo, + cEmpty1, cEmpty2, cEmpty3, cEmpty4, cFoo); + cBar.assertNoCallback(); + assertEquals(nsFoo, + mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).getNetworkSpecifier()); + assertNoCallbacks(cEmpty1, cEmpty2, cEmpty3, cEmpty4, cFoo); + + mWiFiNetworkAgent.disconnect(); + expectOnLost(mWiFiNetworkAgent, cEmpty1, cEmpty2, cEmpty3, cEmpty4, cFoo); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.setNetworkSpecifier(nsBar); + mWiFiNetworkAgent.connect(false); + expectAvailableCallbacksUnvalidatedWithSpecifier(mWiFiNetworkAgent, nsBar, + cEmpty1, cEmpty2, cEmpty3, cEmpty4, cBar); + cFoo.assertNoCallback(); + assertEquals(nsBar, + mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).getNetworkSpecifier()); + + mWiFiNetworkAgent.disconnect(); + expectOnLost(mWiFiNetworkAgent, cEmpty1, cEmpty2, cEmpty3, cEmpty4, cBar); + cFoo.assertNoCallback(); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.setNetworkSpecifier(new ConfidentialMatchAllNetworkSpecifier()); + mWiFiNetworkAgent.connect(false); + expectAvailableCallbacksUnvalidatedWithSpecifier(mWiFiNetworkAgent, null /* specifier */, + cEmpty1, cEmpty2, cEmpty3, cEmpty4, cFoo, cBar); + assertNull( + mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).getNetworkSpecifier()); + + mWiFiNetworkAgent.disconnect(); + expectOnLost(mWiFiNetworkAgent, cEmpty1, cEmpty2, cEmpty3, cEmpty4, cFoo, cBar); + } + + /** + * @return the context's attribution tag + */ + private String getAttributionTag() { + return null; + } + + @Test + public void testInvalidNetworkSpecifier() { + assertThrows(IllegalArgumentException.class, () -> { + NetworkRequest.Builder builder = new NetworkRequest.Builder(); + builder.setNetworkSpecifier(new MatchAllNetworkSpecifier()); + }); + + assertThrows(IllegalArgumentException.class, () -> { + NetworkCapabilities networkCapabilities = new NetworkCapabilities(); + networkCapabilities.addTransportType(TRANSPORT_WIFI) + .setNetworkSpecifier(new MatchAllNetworkSpecifier()); + mService.requestNetwork(Process.INVALID_UID, networkCapabilities, + NetworkRequest.Type.REQUEST.ordinal(), null, 0, null, + ConnectivityManager.TYPE_WIFI, NetworkCallback.FLAG_NONE, + mContext.getPackageName(), getAttributionTag()); + }); + + class NonParcelableSpecifier extends NetworkSpecifier { + @Override + public boolean canBeSatisfiedBy(NetworkSpecifier other) { + return false; + } + }; + class ParcelableSpecifier extends NonParcelableSpecifier implements Parcelable { + @Override public int describeContents() { return 0; } + @Override public void writeToParcel(Parcel p, int flags) {} + } + + final NetworkRequest.Builder builder = + new NetworkRequest.Builder().addTransportType(TRANSPORT_ETHERNET); + assertThrows(ClassCastException.class, () -> { + builder.setNetworkSpecifier(new NonParcelableSpecifier()); + Parcel parcelW = Parcel.obtain(); + builder.build().writeToParcel(parcelW, 0); + }); + + final NetworkRequest nr = + new NetworkRequest.Builder().addTransportType(TRANSPORT_ETHERNET) + .setNetworkSpecifier(new ParcelableSpecifier()) + .build(); + assertNotNull(nr); + + assertThrows(BadParcelableException.class, () -> { + Parcel parcelW = Parcel.obtain(); + nr.writeToParcel(parcelW, 0); + byte[] bytes = parcelW.marshall(); + parcelW.recycle(); + + Parcel parcelR = Parcel.obtain(); + parcelR.unmarshall(bytes, 0, bytes.length); + parcelR.setDataPosition(0); + NetworkRequest rereadNr = NetworkRequest.CREATOR.createFromParcel(parcelR); + }); + } + + @Test + public void testNetworkRequestUidSpoofSecurityException() throws Exception { + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + NetworkRequest networkRequest = newWifiRequestBuilder().build(); + TestNetworkCallback networkCallback = new TestNetworkCallback(); + doThrow(new SecurityException()).when(mAppOpsManager).checkPackage(anyInt(), anyString()); + assertThrows(SecurityException.class, () -> { + mCm.requestNetwork(networkRequest, networkCallback); + }); + } + + @Test + public void testInvalidSignalStrength() { + NetworkRequest r = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_INTERNET) + .addTransportType(TRANSPORT_WIFI) + .setSignalStrength(-75) + .build(); + // Registering a NetworkCallback with signal strength but w/o NETWORK_SIGNAL_STRENGTH_WAKEUP + // permission should get SecurityException. + assertThrows(SecurityException.class, () -> + mCm.registerNetworkCallback(r, new NetworkCallback())); + + assertThrows(SecurityException.class, () -> + mCm.registerNetworkCallback(r, PendingIntent.getService( + mServiceContext, 0 /* requestCode */, new Intent(), FLAG_IMMUTABLE))); + + // Requesting a Network with signal strength should get IllegalArgumentException. + assertThrows(IllegalArgumentException.class, () -> + mCm.requestNetwork(r, new NetworkCallback())); + + assertThrows(IllegalArgumentException.class, () -> + mCm.requestNetwork(r, PendingIntent.getService( + mServiceContext, 0 /* requestCode */, new Intent(), FLAG_IMMUTABLE))); + } + + @Test + public void testRegisterDefaultNetworkCallback() throws Exception { + // NETWORK_SETTINGS is necessary to call registerSystemDefaultNetworkCallback. + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + + final TestNetworkCallback defaultNetworkCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(defaultNetworkCallback); + defaultNetworkCallback.assertNoCallback(); + + final Handler handler = new Handler(ConnectivityThread.getInstanceLooper()); + final TestNetworkCallback systemDefaultCallback = new TestNetworkCallback(); + mCm.registerSystemDefaultNetworkCallback(systemDefaultCallback, handler); + systemDefaultCallback.assertNoCallback(); + + // Create a TRANSPORT_CELLULAR request to keep the mobile interface up + // whenever Wi-Fi is up. Without this, the mobile network agent is + // reaped before any other activity can take place. + final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback(); + final NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + mCm.requestNetwork(cellRequest, cellNetworkCallback); + cellNetworkCallback.assertNoCallback(); + + // Bring up cell and expect CALLBACK_AVAILABLE. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + defaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + systemDefaultCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + assertEquals(defaultNetworkCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + assertEquals(systemDefaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + // Bring up wifi and expect CALLBACK_AVAILABLE. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + cellNetworkCallback.assertNoCallback(); + defaultNetworkCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + systemDefaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + assertEquals(defaultNetworkCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + assertEquals(systemDefaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + // Bring down cell. Expect no default network callback, since it wasn't the default. + mCellNetworkAgent.disconnect(); + cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + defaultNetworkCallback.assertNoCallback(); + systemDefaultCallback.assertNoCallback(); + assertEquals(defaultNetworkCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + assertEquals(systemDefaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + // Bring up cell. Expect no default network callback, since it won't be the default. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + defaultNetworkCallback.assertNoCallback(); + systemDefaultCallback.assertNoCallback(); + assertEquals(defaultNetworkCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + assertEquals(systemDefaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + // Bring down wifi. Expect the default network callback to notified of LOST wifi + // followed by AVAILABLE cell. + mWiFiNetworkAgent.disconnect(); + cellNetworkCallback.assertNoCallback(); + defaultNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + defaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + systemDefaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + systemDefaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + mCellNetworkAgent.disconnect(); + cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + defaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + systemDefaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + waitForIdle(); + assertEquals(null, mCm.getActiveNetwork()); + + mMockVpn.establishForMyUid(); + assertUidRangesUpdatedForMyUid(true); + defaultNetworkCallback.expectAvailableThenValidatedCallbacks(mMockVpn); + systemDefaultCallback.assertNoCallback(); + assertEquals(defaultNetworkCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + assertEquals(null, systemDefaultCallback.getLastAvailableNetwork()); + + mMockVpn.disconnect(); + defaultNetworkCallback.expectCallback(CallbackEntry.LOST, mMockVpn); + systemDefaultCallback.assertNoCallback(); + waitForIdle(); + assertEquals(null, mCm.getActiveNetwork()); + } + + @Test + public void testAdditionalStateCallbacks() throws Exception { + // File a network request for mobile. + final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback(); + final NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + mCm.requestNetwork(cellRequest, cellNetworkCallback); + + // Bring up the mobile network. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + + // We should get onAvailable(), onCapabilitiesChanged(), and + // onLinkPropertiesChanged() in rapid succession. Additionally, we + // should get onCapabilitiesChanged() when the mobile network validates. + cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + + // Update LinkProperties. + final LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("foonet_data0"); + mCellNetworkAgent.sendLinkProperties(lp); + // We should get onLinkPropertiesChanged(). + cellNetworkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, + mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + + // Suspend the network. + mCellNetworkAgent.suspend(); + cellNetworkCallback.expectCapabilitiesWithout(NET_CAPABILITY_NOT_SUSPENDED, + mCellNetworkAgent); + cellNetworkCallback.expectCallback(CallbackEntry.SUSPENDED, mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + assertEquals(NetworkInfo.State.SUSPENDED, mCm.getActiveNetworkInfo().getState()); + + // Register a garden variety default network request. + TestNetworkCallback dfltNetworkCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(dfltNetworkCallback); + // We should get onAvailable(), onCapabilitiesChanged(), onLinkPropertiesChanged(), + // as well as onNetworkSuspended() in rapid succession. + dfltNetworkCallback.expectAvailableAndSuspendedCallbacks(mCellNetworkAgent, true); + dfltNetworkCallback.assertNoCallback(); + mCm.unregisterNetworkCallback(dfltNetworkCallback); + + mCellNetworkAgent.resume(); + cellNetworkCallback.expectCapabilitiesWith(NET_CAPABILITY_NOT_SUSPENDED, + mCellNetworkAgent); + cellNetworkCallback.expectCallback(CallbackEntry.RESUMED, mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + assertEquals(NetworkInfo.State.CONNECTED, mCm.getActiveNetworkInfo().getState()); + + dfltNetworkCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(dfltNetworkCallback); + // This time onNetworkSuspended should not be called. + dfltNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + dfltNetworkCallback.assertNoCallback(); + + mCm.unregisterNetworkCallback(dfltNetworkCallback); + mCm.unregisterNetworkCallback(cellNetworkCallback); + } + + @Test + public void testRegisterPrivilegedDefaultCallbacksRequireNetworkSettings() throws Exception { + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(false /* validated */); + + final Handler handler = new Handler(ConnectivityThread.getInstanceLooper()); + final TestNetworkCallback callback = new TestNetworkCallback(); + assertThrows(SecurityException.class, + () -> mCm.registerSystemDefaultNetworkCallback(callback, handler)); + callback.assertNoCallback(); + assertThrows(SecurityException.class, + () -> mCm.registerDefaultNetworkCallbackForUid(APP1_UID, callback, handler)); + callback.assertNoCallback(); + + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + mCm.registerSystemDefaultNetworkCallback(callback, handler); + callback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + mCm.unregisterNetworkCallback(callback); + + mCm.registerDefaultNetworkCallbackForUid(APP1_UID, callback, handler); + callback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + mCm.unregisterNetworkCallback(callback); + } + + private void setCaptivePortalMode(int mode) { + ContentResolver cr = mServiceContext.getContentResolver(); + Settings.Global.putInt(cr, ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE, mode); + } + + private void setAlwaysOnNetworks(boolean enable) { + ContentResolver cr = mServiceContext.getContentResolver(); + Settings.Global.putInt(cr, ConnectivitySettingsManager.MOBILE_DATA_ALWAYS_ON, + enable ? 1 : 0); + mService.updateAlwaysOnNetworks(); + waitForIdle(); + } + + private void setPrivateDnsSettings(int mode, String specifier) { + ConnectivitySettingsManager.setPrivateDnsMode(mServiceContext, mode); + ConnectivitySettingsManager.setPrivateDnsHostname(mServiceContext, specifier); + mService.updatePrivateDnsSettings(); + waitForIdle(); + } + + private boolean isForegroundNetwork(TestNetworkAgentWrapper network) { + NetworkCapabilities nc = mCm.getNetworkCapabilities(network.getNetwork()); + assertNotNull(nc); + return nc.hasCapability(NET_CAPABILITY_FOREGROUND); + } + + @Test + public void testBackgroundNetworks() throws Exception { + // Create a cellular background request. + grantUsingBackgroundNetworksPermissionForUid(Binder.getCallingUid()); + final TestNetworkCallback cellBgCallback = new TestNetworkCallback(); + mCm.requestBackgroundNetwork(new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(), + cellBgCallback, mCsHandlerThread.getThreadHandler()); + + // Make callbacks for monitoring. + final NetworkRequest request = new NetworkRequest.Builder().build(); + final NetworkRequest fgRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_FOREGROUND).build(); + final TestNetworkCallback callback = new TestNetworkCallback(); + final TestNetworkCallback fgCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + mCm.registerNetworkCallback(fgRequest, fgCallback); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + fgCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + assertTrue(isForegroundNetwork(mCellNetworkAgent)); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + + // When wifi connects, cell lingers. + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + fgCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + fgCallback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + fgCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + assertTrue(isForegroundNetwork(mCellNetworkAgent)); + assertTrue(isForegroundNetwork(mWiFiNetworkAgent)); + + // When lingering is complete, cell is still there but is now in the background. + waitForIdle(); + int timeoutMs = TEST_LINGER_DELAY_MS + TEST_LINGER_DELAY_MS / 4; + fgCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent, timeoutMs); + // Expect a network capabilities update sans FOREGROUND. + callback.expectCapabilitiesWithout(NET_CAPABILITY_FOREGROUND, mCellNetworkAgent); + assertFalse(isForegroundNetwork(mCellNetworkAgent)); + assertTrue(isForegroundNetwork(mWiFiNetworkAgent)); + + // File a cell request and check that cell comes into the foreground. + final NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + final TestNetworkCallback cellCallback = new TestNetworkCallback(); + mCm.requestNetwork(cellRequest, cellCallback); + cellCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + fgCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + // Expect a network capabilities update with FOREGROUND, because the most recent + // request causes its state to change. + cellCallback.expectCapabilitiesWith(NET_CAPABILITY_FOREGROUND, mCellNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_FOREGROUND, mCellNetworkAgent); + assertTrue(isForegroundNetwork(mCellNetworkAgent)); + assertTrue(isForegroundNetwork(mWiFiNetworkAgent)); + + // Release the request. The network immediately goes into the background, since it was not + // lingering. + mCm.unregisterNetworkCallback(cellCallback); + fgCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + // Expect a network capabilities update sans FOREGROUND. + callback.expectCapabilitiesWithout(NET_CAPABILITY_FOREGROUND, mCellNetworkAgent); + assertFalse(isForegroundNetwork(mCellNetworkAgent)); + assertTrue(isForegroundNetwork(mWiFiNetworkAgent)); + + // Disconnect wifi and check that cell is foreground again. + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + fgCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + fgCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + assertTrue(isForegroundNetwork(mCellNetworkAgent)); + + mCm.unregisterNetworkCallback(callback); + mCm.unregisterNetworkCallback(fgCallback); + mCm.unregisterNetworkCallback(cellBgCallback); + } + + @Ignore // This test has instrinsic chances of spurious failures: ignore for continuous testing. + public void benchmarkRequestRegistrationAndCallbackDispatch() throws Exception { + // TODO: turn this unit test into a real benchmarking test. + // Benchmarks connecting and switching performance in the presence of a large number of + // NetworkRequests. + // 1. File NUM_REQUESTS requests. + // 2. Have a network connect. Wait for NUM_REQUESTS onAvailable callbacks to fire. + // 3. Have a new network connect and outscore the previous. Wait for NUM_REQUESTS onLosing + // and NUM_REQUESTS onAvailable callbacks to fire. + // See how long it took. + final int NUM_REQUESTS = 90; + final int REGISTER_TIME_LIMIT_MS = 200; + final int CONNECT_TIME_LIMIT_MS = 60; + final int SWITCH_TIME_LIMIT_MS = 60; + final int UNREGISTER_TIME_LIMIT_MS = 20; + + final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build(); + final NetworkCallback[] callbacks = new NetworkCallback[NUM_REQUESTS]; + final CountDownLatch availableLatch = new CountDownLatch(NUM_REQUESTS); + final CountDownLatch losingLatch = new CountDownLatch(NUM_REQUESTS); + + for (int i = 0; i < NUM_REQUESTS; i++) { + callbacks[i] = new NetworkCallback() { + @Override public void onAvailable(Network n) { availableLatch.countDown(); } + @Override public void onLosing(Network n, int t) { losingLatch.countDown(); } + }; + } + + assertRunsInAtMost("Registering callbacks", REGISTER_TIME_LIMIT_MS, () -> { + for (NetworkCallback cb : callbacks) { + mCm.registerNetworkCallback(request, cb); + } + }); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + // Don't request that the network validate, because otherwise connect() will block until + // the network gets NET_CAPABILITY_VALIDATED, after all the callbacks below have fired, + // and we won't actually measure anything. + mCellNetworkAgent.connect(false); + + long onAvailableDispatchingDuration = durationOf(() -> { + await(availableLatch, 10 * CONNECT_TIME_LIMIT_MS); + }); + Log.d(TAG, String.format("Dispatched %d of %d onAvailable callbacks in %dms", + NUM_REQUESTS - availableLatch.getCount(), NUM_REQUESTS, + onAvailableDispatchingDuration)); + assertTrue(String.format("Dispatching %d onAvailable callbacks in %dms, expected %dms", + NUM_REQUESTS, onAvailableDispatchingDuration, CONNECT_TIME_LIMIT_MS), + onAvailableDispatchingDuration <= CONNECT_TIME_LIMIT_MS); + + // Give wifi a high enough score that we'll linger cell when wifi comes up. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.adjustScore(40); + mWiFiNetworkAgent.connect(false); + + long onLostDispatchingDuration = durationOf(() -> { + await(losingLatch, 10 * SWITCH_TIME_LIMIT_MS); + }); + Log.d(TAG, String.format("Dispatched %d of %d onLosing callbacks in %dms", + NUM_REQUESTS - losingLatch.getCount(), NUM_REQUESTS, onLostDispatchingDuration)); + assertTrue(String.format("Dispatching %d onLosing callbacks in %dms, expected %dms", + NUM_REQUESTS, onLostDispatchingDuration, SWITCH_TIME_LIMIT_MS), + onLostDispatchingDuration <= SWITCH_TIME_LIMIT_MS); + + assertRunsInAtMost("Unregistering callbacks", UNREGISTER_TIME_LIMIT_MS, () -> { + for (NetworkCallback cb : callbacks) { + mCm.unregisterNetworkCallback(cb); + } + }); + } + + @Test + public void testMobileDataAlwaysOn() throws Exception { + grantUsingBackgroundNetworksPermissionForUid(Binder.getCallingUid()); + final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback(); + final NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + mCm.registerNetworkCallback(cellRequest, cellNetworkCallback); + + final HandlerThread handlerThread = new HandlerThread("MobileDataAlwaysOnFactory"); + handlerThread.start(); + NetworkCapabilities filter = new NetworkCapabilities() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED) + .addCapability(NET_CAPABILITY_INTERNET); + final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(), + mServiceContext, "testFactory", filter, mCsHandlerThread); + testFactory.setScoreFilter(40); + + // Register the factory and expect it to start looking for a network. + testFactory.register(); + + try { + testFactory.expectRequestAdd(); + testFactory.assertRequestCountEquals(1); + assertTrue(testFactory.getMyStartRequested()); + + // Bring up wifi. The factory stops looking for a network. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + // Score 60 - 40 penalty for not validated yet, then 60 when it validates + mWiFiNetworkAgent.connect(true); + // Default request and mobile always on request + testFactory.expectRequestAdds(2); + assertFalse(testFactory.getMyStartRequested()); + + // Turn on mobile data always on. The factory starts looking again. + setAlwaysOnNetworks(true); + testFactory.expectRequestAdd(); + testFactory.assertRequestCountEquals(2); + + assertTrue(testFactory.getMyStartRequested()); + + // Bring up cell data and check that the factory stops looking. + assertLength(1, mCm.getAllNetworks()); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + testFactory.expectRequestAdds(2); // Unvalidated and validated + testFactory.assertRequestCountEquals(2); + // The cell network outscores the factory filter, so start is not requested. + assertFalse(testFactory.getMyStartRequested()); + + // Check that cell data stays up. + waitForIdle(); + verifyActiveNetwork(TRANSPORT_WIFI); + assertLength(2, mCm.getAllNetworks()); + + // Turn off mobile data always on and expect the request to disappear... + setAlwaysOnNetworks(false); + testFactory.expectRequestRemove(); + + // ... and cell data to be torn down after nascent network timeout. + cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent, + mService.mNascentDelayMs + TEST_CALLBACK_TIMEOUT_MS); + waitForIdle(); + assertLength(1, mCm.getAllNetworks()); + } finally { + testFactory.terminate(); + mCm.unregisterNetworkCallback(cellNetworkCallback); + handlerThread.quit(); + } + } + + @Test + public void testAvoidBadWifiSetting() throws Exception { + final ContentResolver cr = mServiceContext.getContentResolver(); + final String settingName = ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI; + + mPolicyTracker.mConfigRestrictsAvoidBadWifi = false; + String[] values = new String[] {null, "0", "1"}; + for (int i = 0; i < values.length; i++) { + Settings.Global.putInt(cr, settingName, 1); + mPolicyTracker.reevaluate(); + waitForIdle(); + String msg = String.format("config=false, setting=%s", values[i]); + assertTrue(mService.avoidBadWifi()); + assertFalse(msg, mPolicyTracker.shouldNotifyWifiUnvalidated()); + } + + mPolicyTracker.mConfigRestrictsAvoidBadWifi = true; + + Settings.Global.putInt(cr, settingName, 0); + mPolicyTracker.reevaluate(); + waitForIdle(); + assertFalse(mService.avoidBadWifi()); + assertFalse(mPolicyTracker.shouldNotifyWifiUnvalidated()); + + Settings.Global.putInt(cr, settingName, 1); + mPolicyTracker.reevaluate(); + waitForIdle(); + assertTrue(mService.avoidBadWifi()); + assertFalse(mPolicyTracker.shouldNotifyWifiUnvalidated()); + + Settings.Global.putString(cr, settingName, null); + mPolicyTracker.reevaluate(); + waitForIdle(); + assertFalse(mService.avoidBadWifi()); + assertTrue(mPolicyTracker.shouldNotifyWifiUnvalidated()); + } + + @Ignore("Refactoring in progress b/178071397") + @Test + public void testAvoidBadWifi() throws Exception { + final ContentResolver cr = mServiceContext.getContentResolver(); + + // Pretend we're on a carrier that restricts switching away from bad wifi. + mPolicyTracker.mConfigRestrictsAvoidBadWifi = true; + + // File a request for cell to ensure it doesn't go down. + final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback(); + final NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + mCm.requestNetwork(cellRequest, cellNetworkCallback); + + TestNetworkCallback defaultCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(defaultCallback); + + NetworkRequest validatedWifiRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI) + .addCapability(NET_CAPABILITY_VALIDATED) + .build(); + TestNetworkCallback validatedWifiCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(validatedWifiRequest, validatedWifiCallback); + + Settings.Global.putInt(cr, ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI, 0); + mPolicyTracker.reevaluate(); + + // Bring up validated cell. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + defaultCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + Network cellNetwork = mCellNetworkAgent.getNetwork(); + + // Bring up validated wifi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + validatedWifiCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + Network wifiNetwork = mWiFiNetworkAgent.getNetwork(); + + // Fail validation on wifi. + mWiFiNetworkAgent.setNetworkInvalid(false /* isStrictMode */); + mCm.reportNetworkConnectivity(wifiNetwork, false); + defaultCallback.expectCapabilitiesWithout(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + validatedWifiCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // Because avoid bad wifi is off, we don't switch to cellular. + defaultCallback.assertNoCallback(); + assertFalse(mCm.getNetworkCapabilities(wifiNetwork).hasCapability( + NET_CAPABILITY_VALIDATED)); + assertTrue(mCm.getNetworkCapabilities(cellNetwork).hasCapability( + NET_CAPABILITY_VALIDATED)); + assertEquals(mCm.getActiveNetwork(), wifiNetwork); + + // Simulate switching to a carrier that does not restrict avoiding bad wifi, and expect + // that we switch back to cell. + mPolicyTracker.mConfigRestrictsAvoidBadWifi = false; + mPolicyTracker.reevaluate(); + defaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + assertEquals(mCm.getActiveNetwork(), cellNetwork); + + // Switch back to a restrictive carrier. + mPolicyTracker.mConfigRestrictsAvoidBadWifi = true; + mPolicyTracker.reevaluate(); + defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + assertEquals(mCm.getActiveNetwork(), wifiNetwork); + + // Simulate the user selecting "switch" on the dialog, and check that we switch to cell. + mCm.setAvoidUnvalidated(wifiNetwork); + defaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + assertFalse(mCm.getNetworkCapabilities(wifiNetwork).hasCapability( + NET_CAPABILITY_VALIDATED)); + assertTrue(mCm.getNetworkCapabilities(cellNetwork).hasCapability( + NET_CAPABILITY_VALIDATED)); + assertEquals(mCm.getActiveNetwork(), cellNetwork); + + // Disconnect and reconnect wifi to clear the one-time switch above. + mWiFiNetworkAgent.disconnect(); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + validatedWifiCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + wifiNetwork = mWiFiNetworkAgent.getNetwork(); + + // Fail validation on wifi and expect the dialog to appear. + mWiFiNetworkAgent.setNetworkInvalid(false /* isStrictMode */); + mCm.reportNetworkConnectivity(wifiNetwork, false); + defaultCallback.expectCapabilitiesWithout(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + validatedWifiCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // Simulate the user selecting "switch" and checking the don't ask again checkbox. + Settings.Global.putInt(cr, ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI, 1); + mPolicyTracker.reevaluate(); + + // We now switch to cell. + defaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + assertFalse(mCm.getNetworkCapabilities(wifiNetwork).hasCapability( + NET_CAPABILITY_VALIDATED)); + assertTrue(mCm.getNetworkCapabilities(cellNetwork).hasCapability( + NET_CAPABILITY_VALIDATED)); + assertEquals(mCm.getActiveNetwork(), cellNetwork); + + // Simulate the user turning the cellular fallback setting off and then on. + // We switch to wifi and then to cell. + Settings.Global.putString(cr, ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI, null); + mPolicyTracker.reevaluate(); + defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + assertEquals(mCm.getActiveNetwork(), wifiNetwork); + Settings.Global.putInt(cr, ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI, 1); + mPolicyTracker.reevaluate(); + defaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + assertEquals(mCm.getActiveNetwork(), cellNetwork); + + // If cell goes down, we switch to wifi. + mCellNetworkAgent.disconnect(); + defaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + validatedWifiCallback.assertNoCallback(); + + mCm.unregisterNetworkCallback(cellNetworkCallback); + mCm.unregisterNetworkCallback(validatedWifiCallback); + mCm.unregisterNetworkCallback(defaultCallback); + } + + @Test + public void testMeteredMultipathPreferenceSetting() throws Exception { + final ContentResolver cr = mServiceContext.getContentResolver(); + final String settingName = ConnectivitySettingsManager.NETWORK_METERED_MULTIPATH_PREFERENCE; + + for (int config : Arrays.asList(0, 3, 2)) { + for (String setting: Arrays.asList(null, "0", "2", "1")) { + mPolicyTracker.mConfigMeteredMultipathPreference = config; + Settings.Global.putString(cr, settingName, setting); + mPolicyTracker.reevaluate(); + waitForIdle(); + + final int expected = (setting != null) ? Integer.parseInt(setting) : config; + String msg = String.format("config=%d, setting=%s", config, setting); + assertEquals(msg, expected, mCm.getMultipathPreference(null)); + } + } + } + + /** + * Validate that a satisfied network request does not trigger onUnavailable() once the + * time-out period expires. + */ + @Test + public void testSatisfiedNetworkRequestDoesNotTriggerOnUnavailable() throws Exception { + NetworkRequest nr = new NetworkRequest.Builder().addTransportType( + NetworkCapabilities.TRANSPORT_WIFI).build(); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + mCm.requestNetwork(nr, networkCallback, TEST_REQUEST_TIMEOUT_MS); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + networkCallback.expectAvailableCallbacks(mWiFiNetworkAgent, false, false, false, + TEST_CALLBACK_TIMEOUT_MS); + + // pass timeout and validate that UNAVAILABLE is not called + networkCallback.assertNoCallback(); + } + + /** + * Validate that a satisfied network request followed by a disconnected (lost) network does + * not trigger onUnavailable() once the time-out period expires. + */ + @Test + public void testSatisfiedThenLostNetworkRequestDoesNotTriggerOnUnavailable() throws Exception { + NetworkRequest nr = new NetworkRequest.Builder().addTransportType( + NetworkCapabilities.TRANSPORT_WIFI).build(); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + mCm.requestNetwork(nr, networkCallback, TEST_REQUEST_TIMEOUT_MS); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + networkCallback.expectAvailableCallbacks(mWiFiNetworkAgent, false, false, false, + TEST_CALLBACK_TIMEOUT_MS); + mWiFiNetworkAgent.disconnect(); + networkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // Validate that UNAVAILABLE is not called + networkCallback.assertNoCallback(); + } + + /** + * Validate that when a time-out is specified for a network request the onUnavailable() + * callback is called when time-out expires. Then validate that if network request is + * (somehow) satisfied - the callback isn't called later. + */ + @Test + public void testTimedoutNetworkRequest() throws Exception { + NetworkRequest nr = new NetworkRequest.Builder().addTransportType( + NetworkCapabilities.TRANSPORT_WIFI).build(); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + final int timeoutMs = 10; + mCm.requestNetwork(nr, networkCallback, timeoutMs); + + // pass timeout and validate that UNAVAILABLE is called + networkCallback.expectCallback(CallbackEntry.UNAVAILABLE, (Network) null); + + // create a network satisfying request - validate that request not triggered + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + networkCallback.assertNoCallback(); + } + + /** + * Validate that when a network request is unregistered (cancelled), no posterior event can + * trigger the callback. + */ + @Test + public void testNoCallbackAfterUnregisteredNetworkRequest() throws Exception { + NetworkRequest nr = new NetworkRequest.Builder().addTransportType( + NetworkCapabilities.TRANSPORT_WIFI).build(); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + final int timeoutMs = 10; + + mCm.requestNetwork(nr, networkCallback, timeoutMs); + mCm.unregisterNetworkCallback(networkCallback); + // Regardless of the timeout, unregistering the callback in ConnectivityManager ensures + // that this callback will not be called. + networkCallback.assertNoCallback(); + + // create a network satisfying request - validate that request not triggered + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + networkCallback.assertNoCallback(); + } + + @Test + public void testUnfulfillableNetworkRequest() throws Exception { + runUnfulfillableNetworkRequest(false); + } + + @Test + public void testUnfulfillableNetworkRequestAfterUnregister() throws Exception { + runUnfulfillableNetworkRequest(true); + } + + /** + * Validate the callback flow for a factory releasing a request as unfulfillable. + */ + private void runUnfulfillableNetworkRequest(boolean preUnregister) throws Exception { + NetworkRequest nr = new NetworkRequest.Builder().addTransportType( + NetworkCapabilities.TRANSPORT_WIFI).build(); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + + final HandlerThread handlerThread = new HandlerThread("testUnfulfillableNetworkRequest"); + handlerThread.start(); + NetworkCapabilities filter = new NetworkCapabilities() + .addTransportType(TRANSPORT_WIFI) + .addCapability(NET_CAPABILITY_INTERNET); + final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(), + mServiceContext, "testFactory", filter, mCsHandlerThread); + testFactory.setScoreFilter(40); + + // Register the factory and expect it to receive the default request. + testFactory.register(); + testFactory.expectRequestAdd(); + + // Now file the test request and expect it. + mCm.requestNetwork(nr, networkCallback); + final NetworkRequest newRequest = testFactory.expectRequestAdd().request; + + if (preUnregister) { + mCm.unregisterNetworkCallback(networkCallback); + + // Simulate the factory releasing the request as unfulfillable: no-op since + // the callback has already been unregistered (but a test that no exceptions are + // thrown). + testFactory.triggerUnfulfillable(newRequest); + } else { + // Simulate the factory releasing the request as unfulfillable and expect onUnavailable! + testFactory.triggerUnfulfillable(newRequest); + + networkCallback.expectCallback(CallbackEntry.UNAVAILABLE, (Network) null); + + // unregister network callback - a no-op (since already freed by the + // on-unavailable), but should not fail or throw exceptions. + mCm.unregisterNetworkCallback(networkCallback); + } + + testFactory.expectRequestRemove(); + + testFactory.terminate(); + handlerThread.quit(); + } + + private static class TestKeepaliveCallback extends PacketKeepaliveCallback { + + public enum CallbackType { ON_STARTED, ON_STOPPED, ON_ERROR } + + private class CallbackValue { + public CallbackType callbackType; + public int error; + + public CallbackValue(CallbackType type) { + this.callbackType = type; + this.error = PacketKeepalive.SUCCESS; + assertTrue("onError callback must have error", type != CallbackType.ON_ERROR); + } + + public CallbackValue(CallbackType type, int error) { + this.callbackType = type; + this.error = error; + assertEquals("error can only be set for onError", type, CallbackType.ON_ERROR); + } + + @Override + public boolean equals(Object o) { + return o instanceof CallbackValue && + this.callbackType == ((CallbackValue) o).callbackType && + this.error == ((CallbackValue) o).error; + } + + @Override + public String toString() { + return String.format("%s(%s, %d)", getClass().getSimpleName(), callbackType, error); + } + } + + private final LinkedBlockingQueue mCallbacks = new LinkedBlockingQueue<>(); + + @Override + public void onStarted() { + mCallbacks.add(new CallbackValue(CallbackType.ON_STARTED)); + } + + @Override + public void onStopped() { + mCallbacks.add(new CallbackValue(CallbackType.ON_STOPPED)); + } + + @Override + public void onError(int error) { + mCallbacks.add(new CallbackValue(CallbackType.ON_ERROR, error)); + } + + private void expectCallback(CallbackValue callbackValue) throws InterruptedException { + assertEquals(callbackValue, mCallbacks.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + } + + public void expectStarted() throws Exception { + expectCallback(new CallbackValue(CallbackType.ON_STARTED)); + } + + public void expectStopped() throws Exception { + expectCallback(new CallbackValue(CallbackType.ON_STOPPED)); + } + + public void expectError(int error) throws Exception { + expectCallback(new CallbackValue(CallbackType.ON_ERROR, error)); + } + } + + private static class TestSocketKeepaliveCallback extends SocketKeepalive.Callback { + + public enum CallbackType { ON_STARTED, ON_STOPPED, ON_ERROR }; + + private class CallbackValue { + public CallbackType callbackType; + public int error; + + CallbackValue(CallbackType type) { + this.callbackType = type; + this.error = SocketKeepalive.SUCCESS; + assertTrue("onError callback must have error", type != CallbackType.ON_ERROR); + } + + CallbackValue(CallbackType type, int error) { + this.callbackType = type; + this.error = error; + assertEquals("error can only be set for onError", type, CallbackType.ON_ERROR); + } + + @Override + public boolean equals(Object o) { + return o instanceof CallbackValue + && this.callbackType == ((CallbackValue) o).callbackType + && this.error == ((CallbackValue) o).error; + } + + @Override + public String toString() { + return String.format("%s(%s, %d)", getClass().getSimpleName(), callbackType, + error); + } + } + + private LinkedBlockingQueue mCallbacks = new LinkedBlockingQueue<>(); + private final Executor mExecutor; + + TestSocketKeepaliveCallback(@NonNull Executor executor) { + mExecutor = executor; + } + + @Override + public void onStarted() { + mCallbacks.add(new CallbackValue(CallbackType.ON_STARTED)); + } + + @Override + public void onStopped() { + mCallbacks.add(new CallbackValue(CallbackType.ON_STOPPED)); + } + + @Override + public void onError(int error) { + mCallbacks.add(new CallbackValue(CallbackType.ON_ERROR, error)); + } + + private void expectCallback(CallbackValue callbackValue) throws InterruptedException { + assertEquals(callbackValue, mCallbacks.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + + } + + public void expectStarted() throws InterruptedException { + expectCallback(new CallbackValue(CallbackType.ON_STARTED)); + } + + public void expectStopped() throws InterruptedException { + expectCallback(new CallbackValue(CallbackType.ON_STOPPED)); + } + + public void expectError(int error) throws InterruptedException { + expectCallback(new CallbackValue(CallbackType.ON_ERROR, error)); + } + + public void assertNoCallback() { + waitForIdleSerialExecutor(mExecutor, TIMEOUT_MS); + CallbackValue cv = mCallbacks.peek(); + assertNull("Unexpected callback: " + cv, cv); + } + } + + private Network connectKeepaliveNetwork(LinkProperties lp) throws Exception { + // Ensure the network is disconnected before anything else occurs + if (mWiFiNetworkAgent != null) { + assertNull(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork())); + } + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + ExpectedBroadcast b = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED); + mWiFiNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + mWiFiNetworkAgent.sendLinkProperties(lp); + waitForIdle(); + return mWiFiNetworkAgent.getNetwork(); + } + + @Test + public void testPacketKeepalives() throws Exception { + InetAddress myIPv4 = InetAddress.getByName("192.0.2.129"); + InetAddress notMyIPv4 = InetAddress.getByName("192.0.2.35"); + InetAddress myIPv6 = InetAddress.getByName("2001:db8::1"); + InetAddress dstIPv4 = InetAddress.getByName("8.8.8.8"); + InetAddress dstIPv6 = InetAddress.getByName("2001:4860:4860::8888"); + + final int validKaInterval = 15; + final int invalidKaInterval = 9; + + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("wlan12"); + lp.addLinkAddress(new LinkAddress(myIPv6, 64)); + lp.addLinkAddress(new LinkAddress(myIPv4, 25)); + lp.addRoute(new RouteInfo(InetAddress.getByName("fe80::1234"))); + lp.addRoute(new RouteInfo(InetAddress.getByName("192.0.2.254"))); + + Network notMyNet = new Network(61234); + Network myNet = connectKeepaliveNetwork(lp); + + TestKeepaliveCallback callback = new TestKeepaliveCallback(); + PacketKeepalive ka; + + // Attempt to start keepalives with invalid parameters and check for errors. + ka = mCm.startNattKeepalive(notMyNet, validKaInterval, callback, myIPv4, 1234, dstIPv4); + callback.expectError(PacketKeepalive.ERROR_INVALID_NETWORK); + + ka = mCm.startNattKeepalive(myNet, invalidKaInterval, callback, myIPv4, 1234, dstIPv4); + callback.expectError(PacketKeepalive.ERROR_INVALID_INTERVAL); + + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 1234, dstIPv6); + callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS); + + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv6, 1234, dstIPv4); + callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS); + + // NAT-T is only supported for IPv4. + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv6, 1234, dstIPv6); + callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS); + + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 123456, dstIPv4); + callback.expectError(PacketKeepalive.ERROR_INVALID_PORT); + + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 123456, dstIPv4); + callback.expectError(PacketKeepalive.ERROR_INVALID_PORT); + + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 12345, dstIPv4); + callback.expectError(PacketKeepalive.ERROR_HARDWARE_UNSUPPORTED); + + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 12345, dstIPv4); + callback.expectError(PacketKeepalive.ERROR_HARDWARE_UNSUPPORTED); + + // Check that a started keepalive can be stopped. + mWiFiNetworkAgent.setStartKeepaliveEvent(PacketKeepalive.SUCCESS); + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 12345, dstIPv4); + callback.expectStarted(); + mWiFiNetworkAgent.setStopKeepaliveEvent(PacketKeepalive.SUCCESS); + ka.stop(); + callback.expectStopped(); + + // Check that deleting the IP address stops the keepalive. + LinkProperties bogusLp = new LinkProperties(lp); + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 12345, dstIPv4); + callback.expectStarted(); + bogusLp.removeLinkAddress(new LinkAddress(myIPv4, 25)); + bogusLp.addLinkAddress(new LinkAddress(notMyIPv4, 25)); + mWiFiNetworkAgent.sendLinkProperties(bogusLp); + callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS); + mWiFiNetworkAgent.sendLinkProperties(lp); + + // Check that a started keepalive is stopped correctly when the network disconnects. + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 12345, dstIPv4); + callback.expectStarted(); + mWiFiNetworkAgent.disconnect(); + mWiFiNetworkAgent.expectDisconnected(); + callback.expectError(PacketKeepalive.ERROR_INVALID_NETWORK); + + // ... and that stopping it after that has no adverse effects. + waitForIdle(); + final Network myNetAlias = myNet; + assertNull(mCm.getNetworkCapabilities(myNetAlias)); + ka.stop(); + + // Reconnect. + myNet = connectKeepaliveNetwork(lp); + mWiFiNetworkAgent.setStartKeepaliveEvent(PacketKeepalive.SUCCESS); + + // Check that keepalive slots start from 1 and increment. The first one gets slot 1. + mWiFiNetworkAgent.setExpectedKeepaliveSlot(1); + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 12345, dstIPv4); + callback.expectStarted(); + + // The second one gets slot 2. + mWiFiNetworkAgent.setExpectedKeepaliveSlot(2); + TestKeepaliveCallback callback2 = new TestKeepaliveCallback(); + PacketKeepalive ka2 = mCm.startNattKeepalive( + myNet, validKaInterval, callback2, myIPv4, 6789, dstIPv4); + callback2.expectStarted(); + + // Now stop the first one and create a third. This also gets slot 1. + ka.stop(); + callback.expectStopped(); + + mWiFiNetworkAgent.setExpectedKeepaliveSlot(1); + TestKeepaliveCallback callback3 = new TestKeepaliveCallback(); + PacketKeepalive ka3 = mCm.startNattKeepalive( + myNet, validKaInterval, callback3, myIPv4, 9876, dstIPv4); + callback3.expectStarted(); + + ka2.stop(); + callback2.expectStopped(); + + ka3.stop(); + callback3.expectStopped(); + } + + // Helper method to prepare the executor and run test + private void runTestWithSerialExecutors(ExceptionUtils.ThrowingConsumer functor) + throws Exception { + final ExecutorService executorSingleThread = Executors.newSingleThreadExecutor(); + final Executor executorInline = (Runnable r) -> r.run(); + functor.accept(executorSingleThread); + executorSingleThread.shutdown(); + functor.accept(executorInline); + } + + @Test + public void testNattSocketKeepalives() throws Exception { + runTestWithSerialExecutors(executor -> doTestNattSocketKeepalivesWithExecutor(executor)); + runTestWithSerialExecutors(executor -> doTestNattSocketKeepalivesFdWithExecutor(executor)); + } + + private void doTestNattSocketKeepalivesWithExecutor(Executor executor) throws Exception { + // TODO: 1. Move this outside of ConnectivityServiceTest. + // 2. Make test to verify that Nat-T keepalive socket is created by IpSecService. + // 3. Mock ipsec service. + final InetAddress myIPv4 = InetAddress.getByName("192.0.2.129"); + final InetAddress notMyIPv4 = InetAddress.getByName("192.0.2.35"); + final InetAddress myIPv6 = InetAddress.getByName("2001:db8::1"); + final InetAddress dstIPv4 = InetAddress.getByName("8.8.8.8"); + final InetAddress dstIPv6 = InetAddress.getByName("2001:4860:4860::8888"); + + final int validKaInterval = 15; + final int invalidKaInterval = 9; + + final IpSecManager mIpSec = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE); + final UdpEncapsulationSocket testSocket = mIpSec.openUdpEncapsulationSocket(); + final int srcPort = testSocket.getPort(); + + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("wlan12"); + lp.addLinkAddress(new LinkAddress(myIPv6, 64)); + lp.addLinkAddress(new LinkAddress(myIPv4, 25)); + lp.addRoute(new RouteInfo(InetAddress.getByName("fe80::1234"))); + lp.addRoute(new RouteInfo(InetAddress.getByName("192.0.2.254"))); + + Network notMyNet = new Network(61234); + Network myNet = connectKeepaliveNetwork(lp); + + TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback(executor); + + // Attempt to start keepalives with invalid parameters and check for errors. + // Invalid network. + try (SocketKeepalive ka = mCm.createSocketKeepalive( + notMyNet, testSocket, myIPv4, dstIPv4, executor, callback)) { + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_NETWORK); + } + + // Invalid interval. + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocket, myIPv4, dstIPv4, executor, callback)) { + ka.start(invalidKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_INTERVAL); + } + + // Invalid destination. + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocket, myIPv4, dstIPv6, executor, callback)) { + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS); + } + + // Invalid source; + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocket, myIPv6, dstIPv4, executor, callback)) { + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS); + } + + // NAT-T is only supported for IPv4. + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocket, myIPv6, dstIPv6, executor, callback)) { + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS); + } + + // Basic check before testing started keepalive. + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocket, myIPv4, dstIPv4, executor, callback)) { + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_UNSUPPORTED); + } + + // Check that a started keepalive can be stopped. + mWiFiNetworkAgent.setStartKeepaliveEvent(SocketKeepalive.SUCCESS); + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocket, myIPv4, dstIPv4, executor, callback)) { + ka.start(validKaInterval); + callback.expectStarted(); + mWiFiNetworkAgent.setStopKeepaliveEvent(SocketKeepalive.SUCCESS); + ka.stop(); + callback.expectStopped(); + + // Check that keepalive could be restarted. + ka.start(validKaInterval); + callback.expectStarted(); + ka.stop(); + callback.expectStopped(); + + // Check that keepalive can be restarted without waiting for callback. + ka.start(validKaInterval); + callback.expectStarted(); + ka.stop(); + ka.start(validKaInterval); + callback.expectStopped(); + callback.expectStarted(); + ka.stop(); + callback.expectStopped(); + } + + // Check that deleting the IP address stops the keepalive. + LinkProperties bogusLp = new LinkProperties(lp); + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocket, myIPv4, dstIPv4, executor, callback)) { + ka.start(validKaInterval); + callback.expectStarted(); + bogusLp.removeLinkAddress(new LinkAddress(myIPv4, 25)); + bogusLp.addLinkAddress(new LinkAddress(notMyIPv4, 25)); + mWiFiNetworkAgent.sendLinkProperties(bogusLp); + callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS); + mWiFiNetworkAgent.sendLinkProperties(lp); + } + + // Check that a started keepalive is stopped correctly when the network disconnects. + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocket, myIPv4, dstIPv4, executor, callback)) { + ka.start(validKaInterval); + callback.expectStarted(); + mWiFiNetworkAgent.disconnect(); + mWiFiNetworkAgent.expectDisconnected(); + callback.expectError(SocketKeepalive.ERROR_INVALID_NETWORK); + + // ... and that stopping it after that has no adverse effects. + waitForIdle(); + final Network myNetAlias = myNet; + assertNull(mCm.getNetworkCapabilities(myNetAlias)); + ka.stop(); + callback.assertNoCallback(); + } + + // Reconnect. + myNet = connectKeepaliveNetwork(lp); + mWiFiNetworkAgent.setStartKeepaliveEvent(SocketKeepalive.SUCCESS); + + // Check that a stop followed by network disconnects does not result in crash. + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocket, myIPv4, dstIPv4, executor, callback)) { + ka.start(validKaInterval); + callback.expectStarted(); + // Delay the response of keepalive events in networkAgent long enough to make sure + // the follow-up network disconnection will be processed first. + mWiFiNetworkAgent.setKeepaliveResponseDelay(3 * TIMEOUT_MS); + ka.stop(); + + // Make sure the stop has been processed. Wait for executor idle is needed to prevent + // flaky since the actual stop call to the service is delegated to executor thread. + waitForIdleSerialExecutor(executor, TIMEOUT_MS); + waitForIdle(); + + mWiFiNetworkAgent.disconnect(); + mWiFiNetworkAgent.expectDisconnected(); + callback.expectStopped(); + callback.assertNoCallback(); + } + + // Reconnect. + waitForIdle(); + myNet = connectKeepaliveNetwork(lp); + mWiFiNetworkAgent.setStartKeepaliveEvent(SocketKeepalive.SUCCESS); + + // Check that keepalive slots start from 1 and increment. The first one gets slot 1. + mWiFiNetworkAgent.setExpectedKeepaliveSlot(1); + int srcPort2 = 0; + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocket, myIPv4, dstIPv4, executor, callback)) { + ka.start(validKaInterval); + callback.expectStarted(); + + // The second one gets slot 2. + mWiFiNetworkAgent.setExpectedKeepaliveSlot(2); + final UdpEncapsulationSocket testSocket2 = mIpSec.openUdpEncapsulationSocket(); + srcPort2 = testSocket2.getPort(); + TestSocketKeepaliveCallback callback2 = new TestSocketKeepaliveCallback(executor); + try (SocketKeepalive ka2 = mCm.createSocketKeepalive( + myNet, testSocket2, myIPv4, dstIPv4, executor, callback2)) { + ka2.start(validKaInterval); + callback2.expectStarted(); + + ka.stop(); + callback.expectStopped(); + + ka2.stop(); + callback2.expectStopped(); + + testSocket.close(); + testSocket2.close(); + } + } + + // Check that there is no port leaked after all keepalives and sockets are closed. + // TODO: enable this check after ensuring a valid free port. See b/129512753#comment7. + // assertFalse(isUdpPortInUse(srcPort)); + // assertFalse(isUdpPortInUse(srcPort2)); + + mWiFiNetworkAgent.disconnect(); + mWiFiNetworkAgent.expectDisconnected(); + mWiFiNetworkAgent = null; + } + + @Test + public void testTcpSocketKeepalives() throws Exception { + runTestWithSerialExecutors(executor -> doTestTcpSocketKeepalivesWithExecutor(executor)); + } + + private void doTestTcpSocketKeepalivesWithExecutor(Executor executor) throws Exception { + final int srcPortV4 = 12345; + final int srcPortV6 = 23456; + final InetAddress myIPv4 = InetAddress.getByName("127.0.0.1"); + final InetAddress myIPv6 = InetAddress.getByName("::1"); + + final int validKaInterval = 15; + + final LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("wlan12"); + lp.addLinkAddress(new LinkAddress(myIPv6, 64)); + lp.addLinkAddress(new LinkAddress(myIPv4, 25)); + lp.addRoute(new RouteInfo(InetAddress.getByName("fe80::1234"))); + lp.addRoute(new RouteInfo(InetAddress.getByName("127.0.0.254"))); + + final Network notMyNet = new Network(61234); + final Network myNet = connectKeepaliveNetwork(lp); + + final Socket testSocketV4 = new Socket(); + final Socket testSocketV6 = new Socket(); + + TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback(executor); + + // Attempt to start Tcp keepalives with invalid parameters and check for errors. + // Invalid network. + try (SocketKeepalive ka = mCm.createSocketKeepalive( + notMyNet, testSocketV4, executor, callback)) { + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_NETWORK); + } + + // Invalid Socket (socket is not bound with IPv4 address). + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocketV4, executor, callback)) { + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_SOCKET); + } + + // Invalid Socket (socket is not bound with IPv6 address). + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocketV6, executor, callback)) { + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_SOCKET); + } + + // Bind the socket address + testSocketV4.bind(new InetSocketAddress(myIPv4, srcPortV4)); + testSocketV6.bind(new InetSocketAddress(myIPv6, srcPortV6)); + + // Invalid Socket (socket is bound with IPv4 address). + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocketV4, executor, callback)) { + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_SOCKET); + } + + // Invalid Socket (socket is bound with IPv6 address). + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocketV6, executor, callback)) { + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_SOCKET); + } + + testSocketV4.close(); + testSocketV6.close(); + + mWiFiNetworkAgent.disconnect(); + mWiFiNetworkAgent.expectDisconnected(); + mWiFiNetworkAgent = null; + } + + private void doTestNattSocketKeepalivesFdWithExecutor(Executor executor) throws Exception { + final InetAddress myIPv4 = InetAddress.getByName("192.0.2.129"); + final InetAddress anyIPv4 = InetAddress.getByName("0.0.0.0"); + final InetAddress dstIPv4 = InetAddress.getByName("8.8.8.8"); + final int validKaInterval = 15; + + // Prepare the target network. + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("wlan12"); + lp.addLinkAddress(new LinkAddress(myIPv4, 25)); + lp.addRoute(new RouteInfo(InetAddress.getByName("192.0.2.254"))); + Network myNet = connectKeepaliveNetwork(lp); + mWiFiNetworkAgent.setStartKeepaliveEvent(SocketKeepalive.SUCCESS); + mWiFiNetworkAgent.setStopKeepaliveEvent(SocketKeepalive.SUCCESS); + + TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback(executor); + + // Prepare the target file descriptor, keep only one instance. + final IpSecManager mIpSec = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE); + final UdpEncapsulationSocket testSocket = mIpSec.openUdpEncapsulationSocket(); + final int srcPort = testSocket.getPort(); + final ParcelFileDescriptor testPfd = + ParcelFileDescriptor.dup(testSocket.getFileDescriptor()); + testSocket.close(); + assertTrue(isUdpPortInUse(srcPort)); + + // Start keepalive and explicit make the variable goes out of scope with try-with-resources + // block. + try (SocketKeepalive ka = mCm.createNattKeepalive( + myNet, testPfd, myIPv4, dstIPv4, executor, callback)) { + ka.start(validKaInterval); + callback.expectStarted(); + ka.stop(); + callback.expectStopped(); + } + + // Check that the ParcelFileDescriptor is still valid after keepalive stopped, + // ErrnoException with EBADF will be thrown if the socket is closed when checking local + // address. + assertTrue(isUdpPortInUse(srcPort)); + final InetSocketAddress sa = + (InetSocketAddress) Os.getsockname(testPfd.getFileDescriptor()); + assertEquals(anyIPv4, sa.getAddress()); + + testPfd.close(); + // TODO: enable this check after ensuring a valid free port. See b/129512753#comment7. + // assertFalse(isUdpPortInUse(srcPort)); + + mWiFiNetworkAgent.disconnect(); + mWiFiNetworkAgent.expectDisconnected(); + mWiFiNetworkAgent = null; + } + + private static boolean isUdpPortInUse(int port) { + try (DatagramSocket ignored = new DatagramSocket(port)) { + return false; + } catch (IOException alreadyInUse) { + return true; + } + } + + @Test + public void testGetCaptivePortalServerUrl() throws Exception { + String url = mCm.getCaptivePortalServerUrl(); + assertEquals("http://connectivitycheck.gstatic.com/generate_204", url); + } + + private static class TestNetworkPinner extends NetworkPinner { + public static boolean awaitPin(int timeoutMs) throws InterruptedException { + synchronized(sLock) { + if (sNetwork == null) { + sLock.wait(timeoutMs); + } + return sNetwork != null; + } + } + + public static boolean awaitUnpin(int timeoutMs) throws InterruptedException { + synchronized(sLock) { + if (sNetwork != null) { + sLock.wait(timeoutMs); + } + return sNetwork == null; + } + } + } + + private void assertPinnedToWifiWithCellDefault() { + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getBoundNetworkForProcess()); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + } + + private void assertPinnedToWifiWithWifiDefault() { + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getBoundNetworkForProcess()); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + } + + private void assertNotPinnedToWifi() { + assertNull(mCm.getBoundNetworkForProcess()); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + } + + @Test + public void testNetworkPinner() throws Exception { + NetworkRequest wifiRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI) + .build(); + assertNull(mCm.getBoundNetworkForProcess()); + + TestNetworkPinner.pin(mServiceContext, wifiRequest); + assertNull(mCm.getBoundNetworkForProcess()); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + + // When wi-fi connects, expect to be pinned. + assertTrue(TestNetworkPinner.awaitPin(100)); + assertPinnedToWifiWithCellDefault(); + + // Disconnect and expect the pin to drop. + mWiFiNetworkAgent.disconnect(); + assertTrue(TestNetworkPinner.awaitUnpin(100)); + assertNotPinnedToWifi(); + + // Reconnecting does not cause the pin to come back. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + assertFalse(TestNetworkPinner.awaitPin(100)); + assertNotPinnedToWifi(); + + // Pinning while connected causes the pin to take effect immediately. + TestNetworkPinner.pin(mServiceContext, wifiRequest); + assertTrue(TestNetworkPinner.awaitPin(100)); + assertPinnedToWifiWithCellDefault(); + + // Explicitly unpin and expect to use the default network again. + TestNetworkPinner.unpin(); + assertNotPinnedToWifi(); + + // Disconnect cell and wifi. + ExpectedBroadcast b = registerConnectivityBroadcast(3); // cell down, wifi up, wifi down. + mCellNetworkAgent.disconnect(); + mWiFiNetworkAgent.disconnect(); + b.expectBroadcast(); + + // Pinning takes effect even if the pinned network is the default when the pin is set... + TestNetworkPinner.pin(mServiceContext, wifiRequest); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + assertTrue(TestNetworkPinner.awaitPin(100)); + assertPinnedToWifiWithWifiDefault(); + + // ... and is maintained even when that network is no longer the default. + b = registerConnectivityBroadcast(1); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mCellNetworkAgent.connect(true); + b.expectBroadcast(); + assertPinnedToWifiWithCellDefault(); + } + + @Test + public void testNetworkCallbackMaximum() throws Exception { + final int MAX_REQUESTS = 100; + final int CALLBACKS = 89; + final int INTENTS = 11; + final int SYSTEM_ONLY_MAX_REQUESTS = 250; + assertEquals(MAX_REQUESTS, CALLBACKS + INTENTS); + + NetworkRequest networkRequest = new NetworkRequest.Builder().build(); + ArrayList registered = new ArrayList<>(); + + int j = 0; + while (j++ < CALLBACKS / 2) { + NetworkCallback cb = new NetworkCallback(); + mCm.requestNetwork(networkRequest, cb); + registered.add(cb); + } + while (j++ < CALLBACKS) { + NetworkCallback cb = new NetworkCallback(); + mCm.registerNetworkCallback(networkRequest, cb); + registered.add(cb); + } + j = 0; + while (j++ < INTENTS / 2) { + final PendingIntent pi = PendingIntent.getBroadcast(mContext, 0 /* requestCode */, + new Intent("a" + j), FLAG_IMMUTABLE); + mCm.requestNetwork(networkRequest, pi); + registered.add(pi); + } + while (j++ < INTENTS) { + final PendingIntent pi = PendingIntent.getBroadcast(mContext, 0 /* requestCode */, + new Intent("b" + j), FLAG_IMMUTABLE); + mCm.registerNetworkCallback(networkRequest, pi); + registered.add(pi); + } + + // Test that the limit is enforced when MAX_REQUESTS simultaneous requests are added. + assertThrows(TooManyRequestsException.class, () -> + mCm.requestNetwork(networkRequest, new NetworkCallback()) + ); + assertThrows(TooManyRequestsException.class, () -> + mCm.registerNetworkCallback(networkRequest, new NetworkCallback()) + ); + assertThrows(TooManyRequestsException.class, () -> + mCm.requestNetwork(networkRequest, + PendingIntent.getBroadcast(mContext, 0 /* requestCode */, + new Intent("c"), FLAG_IMMUTABLE)) + ); + assertThrows(TooManyRequestsException.class, () -> + mCm.registerNetworkCallback(networkRequest, + PendingIntent.getBroadcast(mContext, 0 /* requestCode */, + new Intent("d"), FLAG_IMMUTABLE)) + ); + + // The system gets another SYSTEM_ONLY_MAX_REQUESTS slots. + final Handler handler = new Handler(ConnectivityThread.getInstanceLooper()); + withPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, () -> { + ArrayList systemRegistered = new ArrayList<>(); + for (int i = 0; i < SYSTEM_ONLY_MAX_REQUESTS - 1; i++) { + NetworkCallback cb = new NetworkCallback(); + if (i % 2 == 0) { + mCm.registerDefaultNetworkCallbackForUid(1000000 + i, cb, handler); + } else { + mCm.registerNetworkCallback(networkRequest, cb); + } + systemRegistered.add(cb); + } + waitForIdle(); + + assertThrows(TooManyRequestsException.class, () -> + mCm.registerDefaultNetworkCallbackForUid(1001042, new NetworkCallback(), + handler)); + assertThrows(TooManyRequestsException.class, () -> + mCm.registerNetworkCallback(networkRequest, new NetworkCallback())); + + for (NetworkCallback callback : systemRegistered) { + mCm.unregisterNetworkCallback(callback); + } + waitForIdle(); // Wait for requests to be unregistered before giving up the permission. + }); + + for (Object o : registered) { + if (o instanceof NetworkCallback) { + mCm.unregisterNetworkCallback((NetworkCallback)o); + } + if (o instanceof PendingIntent) { + mCm.unregisterNetworkCallback((PendingIntent)o); + } + } + waitForIdle(); + + // Test that the limit is not hit when MAX_REQUESTS requests are added and removed. + for (int i = 0; i < MAX_REQUESTS; i++) { + NetworkCallback networkCallback = new NetworkCallback(); + mCm.requestNetwork(networkRequest, networkCallback); + mCm.unregisterNetworkCallback(networkCallback); + } + waitForIdle(); + + for (int i = 0; i < MAX_REQUESTS; i++) { + NetworkCallback networkCallback = new NetworkCallback(); + mCm.registerNetworkCallback(networkRequest, networkCallback); + mCm.unregisterNetworkCallback(networkCallback); + } + waitForIdle(); + + for (int i = 0; i < MAX_REQUESTS; i++) { + NetworkCallback networkCallback = new NetworkCallback(); + mCm.registerDefaultNetworkCallback(networkCallback); + mCm.unregisterNetworkCallback(networkCallback); + } + waitForIdle(); + + for (int i = 0; i < MAX_REQUESTS; i++) { + NetworkCallback networkCallback = new NetworkCallback(); + mCm.registerDefaultNetworkCallback(networkCallback); + mCm.unregisterNetworkCallback(networkCallback); + } + waitForIdle(); + + withPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, () -> { + for (int i = 0; i < MAX_REQUESTS; i++) { + NetworkCallback networkCallback = new NetworkCallback(); + mCm.registerDefaultNetworkCallbackForUid(1000000 + i, networkCallback, + new Handler(ConnectivityThread.getInstanceLooper())); + mCm.unregisterNetworkCallback(networkCallback); + } + }); + waitForIdle(); + + for (int i = 0; i < MAX_REQUESTS; i++) { + final PendingIntent pendingIntent = PendingIntent.getBroadcast( + mContext, 0 /* requestCode */, new Intent("e" + i), FLAG_IMMUTABLE); + mCm.requestNetwork(networkRequest, pendingIntent); + mCm.unregisterNetworkCallback(pendingIntent); + } + waitForIdle(); + + for (int i = 0; i < MAX_REQUESTS; i++) { + final PendingIntent pendingIntent = PendingIntent.getBroadcast( + mContext, 0 /* requestCode */, new Intent("f" + i), FLAG_IMMUTABLE); + mCm.registerNetworkCallback(networkRequest, pendingIntent); + mCm.unregisterNetworkCallback(pendingIntent); + } + } + + @Test + public void testNetworkInfoOfTypeNone() throws Exception { + ExpectedBroadcast b = registerConnectivityBroadcast(1); + + verifyNoNetwork(); + TestNetworkAgentWrapper wifiAware = new TestNetworkAgentWrapper(TRANSPORT_WIFI_AWARE); + assertNull(mCm.getActiveNetworkInfo()); + + Network[] allNetworks = mCm.getAllNetworks(); + assertLength(1, allNetworks); + Network network = allNetworks[0]; + NetworkCapabilities capabilities = mCm.getNetworkCapabilities(network); + assertTrue(capabilities.hasTransport(TRANSPORT_WIFI_AWARE)); + + final NetworkRequest request = + new NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI_AWARE).build(); + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + // Bring up wifi aware network. + wifiAware.connect(false, false, false /* isStrictMode */); + callback.expectAvailableCallbacksUnvalidated(wifiAware); + + assertNull(mCm.getActiveNetworkInfo()); + assertNull(mCm.getActiveNetwork()); + // TODO: getAllNetworkInfo is dirty and returns a non-empty array right from the start + // of this test. Fix it and uncomment the assert below. + //assertEmpty(mCm.getAllNetworkInfo()); + + // Disconnect wifi aware network. + wifiAware.disconnect(); + callback.expectCallbackThat(TIMEOUT_MS, (info) -> info instanceof CallbackEntry.Lost); + mCm.unregisterNetworkCallback(callback); + + verifyNoNetwork(); + b.expectNoBroadcast(10); + } + + @Test + public void testDeprecatedAndUnsupportedOperations() throws Exception { + final int TYPE_NONE = ConnectivityManager.TYPE_NONE; + assertNull(mCm.getNetworkInfo(TYPE_NONE)); + assertNull(mCm.getNetworkForType(TYPE_NONE)); + assertNull(mCm.getLinkProperties(TYPE_NONE)); + assertFalse(mCm.isNetworkSupported(TYPE_NONE)); + + assertThrows(IllegalArgumentException.class, + () -> mCm.networkCapabilitiesForType(TYPE_NONE)); + + Class unsupported = UnsupportedOperationException.class; + assertThrows(unsupported, () -> mCm.startUsingNetworkFeature(TYPE_WIFI, "")); + assertThrows(unsupported, () -> mCm.stopUsingNetworkFeature(TYPE_WIFI, "")); + // TODO: let test context have configuration application target sdk version + // and test that pre-M requesting for TYPE_NONE sends back APN_REQUEST_FAILED + assertThrows(unsupported, () -> mCm.startUsingNetworkFeature(TYPE_NONE, "")); + assertThrows(unsupported, () -> mCm.stopUsingNetworkFeature(TYPE_NONE, "")); + assertThrows(unsupported, () -> mCm.requestRouteToHostAddress(TYPE_NONE, null)); + } + + @Test + public void testLinkPropertiesEnsuresDirectlyConnectedRoutes() throws Exception { + final NetworkRequest networkRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).build(); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(networkRequest, networkCallback); + + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(WIFI_IFNAME); + LinkAddress myIpv4Address = new LinkAddress("192.168.12.3/24"); + RouteInfo myIpv4DefaultRoute = new RouteInfo((IpPrefix) null, + InetAddresses.parseNumericAddress("192.168.12.1"), lp.getInterfaceName()); + lp.addLinkAddress(myIpv4Address); + lp.addRoute(myIpv4DefaultRoute); + + // Verify direct routes are added when network agent is first registered in + // ConnectivityService. + TestNetworkAgentWrapper networkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, lp); + networkAgent.connect(true); + networkCallback.expectCallback(CallbackEntry.AVAILABLE, networkAgent); + networkCallback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED, networkAgent); + CallbackEntry.LinkPropertiesChanged cbi = + networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, + networkAgent); + networkCallback.expectCallback(CallbackEntry.BLOCKED_STATUS, networkAgent); + networkCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, networkAgent); + networkCallback.assertNoCallback(); + checkDirectlyConnectedRoutes(cbi.getLp(), Arrays.asList(myIpv4Address), + Arrays.asList(myIpv4DefaultRoute)); + checkDirectlyConnectedRoutes(mCm.getLinkProperties(networkAgent.getNetwork()), + Arrays.asList(myIpv4Address), Arrays.asList(myIpv4DefaultRoute)); + + // Verify direct routes are added during subsequent link properties updates. + LinkProperties newLp = new LinkProperties(lp); + LinkAddress myIpv6Address1 = new LinkAddress("fe80::cafe/64"); + LinkAddress myIpv6Address2 = new LinkAddress("2001:db8::2/64"); + newLp.addLinkAddress(myIpv6Address1); + newLp.addLinkAddress(myIpv6Address2); + networkAgent.sendLinkProperties(newLp); + cbi = networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, networkAgent); + networkCallback.assertNoCallback(); + checkDirectlyConnectedRoutes(cbi.getLp(), + Arrays.asList(myIpv4Address, myIpv6Address1, myIpv6Address2), + Arrays.asList(myIpv4DefaultRoute)); + mCm.unregisterNetworkCallback(networkCallback); + } + + private void assertSameElementsNoDuplicates(T[] expected, T[] actual) { + // Easier to implement than a proper "assertSameElements" method that also correctly deals + // with duplicates. + final String msg = Arrays.toString(expected) + " != " + Arrays.toString(actual); + assertEquals(msg, expected.length, actual.length); + Set expectedSet = new ArraySet<>(Arrays.asList(expected)); + assertEquals("expected contains duplicates", expectedSet.size(), expected.length); + // actual cannot have duplicates because it's the same length and has the same elements. + Set actualSet = new ArraySet<>(Arrays.asList(actual)); + assertEquals(expectedSet, actualSet); + } + + private void expectNetworkStatus(Network[] networks, String defaultIface, + Integer vpnUid, String vpnIfname, String[] underlyingIfaces) throws Exception { + ArgumentCaptor> networksCaptor = ArgumentCaptor.forClass(List.class); + ArgumentCaptor> vpnInfosCaptor = + ArgumentCaptor.forClass(List.class); + + verify(mStatsManager, atLeastOnce()).notifyNetworkStatus(networksCaptor.capture(), + any(List.class), eq(defaultIface), vpnInfosCaptor.capture()); + + assertSameElementsNoDuplicates(networksCaptor.getValue().toArray(), networks); + + UnderlyingNetworkInfo[] infos = + vpnInfosCaptor.getValue().toArray(new UnderlyingNetworkInfo[0]); + if (vpnUid != null) { + assertEquals("Should have exactly one VPN:", 1, infos.length); + UnderlyingNetworkInfo info = infos[0]; + assertEquals("Unexpected VPN owner:", (int) vpnUid, info.getOwnerUid()); + assertEquals("Unexpected VPN interface:", vpnIfname, info.getIface()); + assertSameElementsNoDuplicates(underlyingIfaces, + info.getUnderlyingIfaces().toArray(new String[0])); + } else { + assertEquals(0, infos.length); + return; + } + } + + private void expectNetworkStatus( + Network[] networks, String defaultIface) throws Exception { + expectNetworkStatus(networks, defaultIface, null, null, new String[0]); + } + + @Test + public void testStatsIfacesChanged() throws Exception { + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + + final Network[] onlyCell = new Network[] {mCellNetworkAgent.getNetwork()}; + final Network[] onlyWifi = new Network[] {mWiFiNetworkAgent.getNetwork()}; + + LinkProperties cellLp = new LinkProperties(); + cellLp.setInterfaceName(MOBILE_IFNAME); + LinkProperties wifiLp = new LinkProperties(); + wifiLp.setInterfaceName(WIFI_IFNAME); + + // Simple connection should have updated ifaces + mCellNetworkAgent.connect(false); + mCellNetworkAgent.sendLinkProperties(cellLp); + waitForIdle(); + expectNetworkStatus(onlyCell, MOBILE_IFNAME); + reset(mStatsManager); + + // Default network switch should update ifaces. + mWiFiNetworkAgent.connect(false); + mWiFiNetworkAgent.sendLinkProperties(wifiLp); + waitForIdle(); + assertEquals(wifiLp, mService.getActiveLinkProperties()); + expectNetworkStatus(onlyWifi, WIFI_IFNAME); + reset(mStatsManager); + + // Disconnect should update ifaces. + mWiFiNetworkAgent.disconnect(); + waitForIdle(); + expectNetworkStatus(onlyCell, MOBILE_IFNAME); + reset(mStatsManager); + + // Metered change should update ifaces + mCellNetworkAgent.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED); + waitForIdle(); + expectNetworkStatus(onlyCell, MOBILE_IFNAME); + reset(mStatsManager); + + mCellNetworkAgent.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED); + waitForIdle(); + expectNetworkStatus(onlyCell, MOBILE_IFNAME); + reset(mStatsManager); + + // Temp metered change shouldn't update ifaces + mCellNetworkAgent.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED); + waitForIdle(); + verify(mStatsManager, never()).notifyNetworkStatus(eq(Arrays.asList(onlyCell)), + any(List.class), eq(MOBILE_IFNAME), any(List.class)); + reset(mStatsManager); + + // Roaming change should update ifaces + mCellNetworkAgent.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING); + waitForIdle(); + expectNetworkStatus(onlyCell, MOBILE_IFNAME); + reset(mStatsManager); + + // Test VPNs. + final LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(VPN_IFNAME); + + mMockVpn.establishForMyUid(lp); + assertUidRangesUpdatedForMyUid(true); + + final Network[] cellAndVpn = new Network[] { + mCellNetworkAgent.getNetwork(), mMockVpn.getNetwork()}; + + // A VPN with default (null) underlying networks sets the underlying network's interfaces... + expectNetworkStatus(cellAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME, + new String[]{MOBILE_IFNAME}); + + // ...and updates them as the default network switches. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + mWiFiNetworkAgent.sendLinkProperties(wifiLp); + final Network[] onlyNull = new Network[]{null}; + final Network[] wifiAndVpn = new Network[] { + mWiFiNetworkAgent.getNetwork(), mMockVpn.getNetwork()}; + final Network[] cellAndWifi = new Network[] { + mCellNetworkAgent.getNetwork(), mWiFiNetworkAgent.getNetwork()}; + final Network[] cellNullAndWifi = new Network[] { + mCellNetworkAgent.getNetwork(), null, mWiFiNetworkAgent.getNetwork()}; + + waitForIdle(); + assertEquals(wifiLp, mService.getActiveLinkProperties()); + expectNetworkStatus(wifiAndVpn, WIFI_IFNAME, Process.myUid(), VPN_IFNAME, + new String[]{WIFI_IFNAME}); + reset(mStatsManager); + + // A VPN that sets its underlying networks passes the underlying interfaces, and influences + // the default interface sent to NetworkStatsService by virtue of applying to the system + // server UID (or, in this test, to the test's UID). This is the reason for sending + // MOBILE_IFNAME even though the default network is wifi. + // TODO: fix this to pass in the actual default network interface. Whether or not the VPN + // applies to the system server UID should not have any bearing on network stats. + mMockVpn.setUnderlyingNetworks(onlyCell); + waitForIdle(); + expectNetworkStatus(wifiAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME, + new String[]{MOBILE_IFNAME}); + reset(mStatsManager); + + mMockVpn.setUnderlyingNetworks(cellAndWifi); + waitForIdle(); + expectNetworkStatus(wifiAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME, + new String[]{MOBILE_IFNAME, WIFI_IFNAME}); + reset(mStatsManager); + + // Null underlying networks are ignored. + mMockVpn.setUnderlyingNetworks(cellNullAndWifi); + waitForIdle(); + expectNetworkStatus(wifiAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME, + new String[]{MOBILE_IFNAME, WIFI_IFNAME}); + reset(mStatsManager); + + // If an underlying network disconnects, that interface should no longer be underlying. + // This doesn't actually work because disconnectAndDestroyNetwork only notifies + // NetworkStatsService before the underlying network is actually removed. So the underlying + // network will only be removed if notifyIfacesChangedForNetworkStats is called again. This + // could result in incorrect data usage measurements if the interface used by the + // disconnected network is reused by a system component that does not register an agent for + // it (e.g., tethering). + mCellNetworkAgent.disconnect(); + waitForIdle(); + assertNull(mService.getLinkProperties(mCellNetworkAgent.getNetwork())); + expectNetworkStatus(wifiAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME, + new String[]{MOBILE_IFNAME, WIFI_IFNAME}); + + // Confirm that we never tell NetworkStatsService that cell is no longer the underlying + // network for the VPN... + verify(mStatsManager, never()).notifyNetworkStatus(any(List.class), + any(List.class), any() /* anyString() doesn't match null */, + argThat(infos -> infos.get(0).getUnderlyingIfaces().size() == 1 + && WIFI_IFNAME.equals(infos.get(0).getUnderlyingIfaces().get(0)))); + verifyNoMoreInteractions(mStatsManager); + reset(mStatsManager); + + // ... but if something else happens that causes notifyIfacesChangedForNetworkStats to be + // called again, it does. For example, connect Ethernet, but with a low score, such that it + // does not become the default network. + mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET); + mEthernetNetworkAgent.adjustScore(-40); + mEthernetNetworkAgent.connect(false); + waitForIdle(); + verify(mStatsManager).notifyNetworkStatus(any(List.class), + any(List.class), any() /* anyString() doesn't match null */, + argThat(vpnInfos -> vpnInfos.get(0).getUnderlyingIfaces().size() == 1 + && WIFI_IFNAME.equals(vpnInfos.get(0).getUnderlyingIfaces().get(0)))); + mEthernetNetworkAgent.disconnect(); + waitForIdle(); + reset(mStatsManager); + + // When a VPN declares no underlying networks (i.e., no connectivity), getAllVpnInfo + // does not return the VPN, so CS does not pass it to NetworkStatsService. This causes + // NetworkStatsFactory#adjustForTunAnd464Xlat not to attempt any VPN data migration, which + // is probably a performance improvement (though it's very unlikely that a VPN would declare + // no underlying networks). + // Also, for the same reason as above, the active interface passed in is null. + mMockVpn.setUnderlyingNetworks(new Network[0]); + waitForIdle(); + expectNetworkStatus(wifiAndVpn, null); + reset(mStatsManager); + + // Specifying only a null underlying network is the same as no networks. + mMockVpn.setUnderlyingNetworks(onlyNull); + waitForIdle(); + expectNetworkStatus(wifiAndVpn, null); + reset(mStatsManager); + + // Specifying networks that are all disconnected is the same as specifying no networks. + mMockVpn.setUnderlyingNetworks(onlyCell); + waitForIdle(); + expectNetworkStatus(wifiAndVpn, null); + reset(mStatsManager); + + // Passing in null again means follow the default network again. + mMockVpn.setUnderlyingNetworks(null); + waitForIdle(); + expectNetworkStatus(wifiAndVpn, WIFI_IFNAME, Process.myUid(), VPN_IFNAME, + new String[]{WIFI_IFNAME}); + reset(mStatsManager); + } + + @Test + public void testBasicDnsConfigurationPushed() throws Exception { + setPrivateDnsSettings(PRIVATE_DNS_MODE_OPPORTUNISTIC, "ignored.example.com"); + + // Clear any interactions that occur as a result of CS starting up. + reset(mMockDnsResolver); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + waitForIdle(); + verify(mMockDnsResolver, never()).setResolverConfiguration(any()); + verifyNoMoreInteractions(mMockDnsResolver); + + final LinkProperties cellLp = new LinkProperties(); + cellLp.setInterfaceName(MOBILE_IFNAME); + // Add IPv4 and IPv6 default routes, because DNS-over-TLS code does + // "is-reachable" testing in order to not program netd with unreachable + // nameservers that it might try repeated to validate. + cellLp.addLinkAddress(new LinkAddress("192.0.2.4/24")); + cellLp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("192.0.2.4"), + MOBILE_IFNAME)); + cellLp.addLinkAddress(new LinkAddress("2001:db8:1::1/64")); + cellLp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("2001:db8:1::1"), + MOBILE_IFNAME)); + mCellNetworkAgent.sendLinkProperties(cellLp); + mCellNetworkAgent.connect(false); + waitForIdle(); + + verify(mMockDnsResolver, times(1)).createNetworkCache( + eq(mCellNetworkAgent.getNetwork().netId)); + // CS tells dnsresolver about the empty DNS config for this network. + verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration(any()); + reset(mMockDnsResolver); + + cellLp.addDnsServer(InetAddress.getByName("2001:db8::1")); + mCellNetworkAgent.sendLinkProperties(cellLp); + waitForIdle(); + verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration( + mResolverParamsParcelCaptor.capture()); + ResolverParamsParcel resolvrParams = mResolverParamsParcelCaptor.getValue(); + assertEquals(1, resolvrParams.servers.length); + assertTrue(ArrayUtils.contains(resolvrParams.servers, "2001:db8::1")); + // Opportunistic mode. + assertTrue(ArrayUtils.contains(resolvrParams.tlsServers, "2001:db8::1")); + reset(mMockDnsResolver); + + cellLp.addDnsServer(InetAddress.getByName("192.0.2.1")); + mCellNetworkAgent.sendLinkProperties(cellLp); + waitForIdle(); + verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration( + mResolverParamsParcelCaptor.capture()); + resolvrParams = mResolverParamsParcelCaptor.getValue(); + assertEquals(2, resolvrParams.servers.length); + assertTrue(ArrayUtils.containsAll(resolvrParams.servers, + new String[]{"2001:db8::1", "192.0.2.1"})); + // Opportunistic mode. + assertEquals(2, resolvrParams.tlsServers.length); + assertTrue(ArrayUtils.containsAll(resolvrParams.tlsServers, + new String[]{"2001:db8::1", "192.0.2.1"})); + reset(mMockDnsResolver); + + final String TLS_SPECIFIER = "tls.example.com"; + final String TLS_SERVER6 = "2001:db8:53::53"; + final InetAddress[] TLS_IPS = new InetAddress[]{ InetAddress.getByName(TLS_SERVER6) }; + final String[] TLS_SERVERS = new String[]{ TLS_SERVER6 }; + mCellNetworkAgent.mNmCallbacks.notifyPrivateDnsConfigResolved( + new PrivateDnsConfig(TLS_SPECIFIER, TLS_IPS).toParcel()); + + waitForIdle(); + verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration( + mResolverParamsParcelCaptor.capture()); + resolvrParams = mResolverParamsParcelCaptor.getValue(); + assertEquals(2, resolvrParams.servers.length); + assertTrue(ArrayUtils.containsAll(resolvrParams.servers, + new String[]{"2001:db8::1", "192.0.2.1"})); + reset(mMockDnsResolver); + } + + @Test + public void testDnsConfigurationTransTypesPushed() throws Exception { + // Clear any interactions that occur as a result of CS starting up. + reset(mMockDnsResolver); + + final NetworkRequest request = new NetworkRequest.Builder() + .clearCapabilities().addCapability(NET_CAPABILITY_INTERNET) + .build(); + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + verify(mMockDnsResolver, times(1)).createNetworkCache( + eq(mWiFiNetworkAgent.getNetwork().netId)); + verify(mMockDnsResolver, times(2)).setResolverConfiguration( + mResolverParamsParcelCaptor.capture()); + final ResolverParamsParcel resolverParams = mResolverParamsParcelCaptor.getValue(); + assertContainsExactly(resolverParams.transportTypes, TRANSPORT_WIFI); + reset(mMockDnsResolver); + } + + @Test + public void testPrivateDnsNotification() throws Exception { + NetworkRequest request = new NetworkRequest.Builder() + .clearCapabilities().addCapability(NET_CAPABILITY_INTERNET) + .build(); + TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + // Bring up wifi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + // Private DNS resolution failed, checking if the notification will be shown or not. + mWiFiNetworkAgent.setNetworkInvalid(true /* isStrictMode */); + mWiFiNetworkAgent.mNetworkMonitor.forceReevaluation(Process.myUid()); + waitForIdle(); + // If network validation failed, NetworkMonitor will re-evaluate the network. + // ConnectivityService should filter the redundant notification. This part is trying to + // simulate that situation and check if ConnectivityService could filter that case. + mWiFiNetworkAgent.mNetworkMonitor.forceReevaluation(Process.myUid()); + waitForIdle(); + verify(mNotificationManager, timeout(TIMEOUT_MS).times(1)).notify(anyString(), + eq(NotificationType.PRIVATE_DNS_BROKEN.eventId), any()); + // If private DNS resolution successful, the PRIVATE_DNS_BROKEN notification shouldn't be + // shown. + mWiFiNetworkAgent.setNetworkValid(true /* isStrictMode */); + mWiFiNetworkAgent.mNetworkMonitor.forceReevaluation(Process.myUid()); + waitForIdle(); + verify(mNotificationManager, timeout(TIMEOUT_MS).times(1)).cancel(anyString(), + eq(NotificationType.PRIVATE_DNS_BROKEN.eventId)); + // If private DNS resolution failed again, the PRIVATE_DNS_BROKEN notification should be + // shown again. + mWiFiNetworkAgent.setNetworkInvalid(true /* isStrictMode */); + mWiFiNetworkAgent.mNetworkMonitor.forceReevaluation(Process.myUid()); + waitForIdle(); + verify(mNotificationManager, timeout(TIMEOUT_MS).times(2)).notify(anyString(), + eq(NotificationType.PRIVATE_DNS_BROKEN.eventId), any()); + } + + @Test + public void testPrivateDnsSettingsChange() throws Exception { + // Clear any interactions that occur as a result of CS starting up. + reset(mMockDnsResolver); + + // The default on Android is opportunistic mode ("Automatic"). + setPrivateDnsSettings(PRIVATE_DNS_MODE_OPPORTUNISTIC, "ignored.example.com"); + + final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback(); + final NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + mCm.requestNetwork(cellRequest, cellNetworkCallback); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + waitForIdle(); + // CS tells netd about the empty DNS config for this network. + verify(mMockDnsResolver, never()).setResolverConfiguration(any()); + verifyNoMoreInteractions(mMockDnsResolver); + + final LinkProperties cellLp = new LinkProperties(); + cellLp.setInterfaceName(MOBILE_IFNAME); + // Add IPv4 and IPv6 default routes, because DNS-over-TLS code does + // "is-reachable" testing in order to not program netd with unreachable + // nameservers that it might try repeated to validate. + cellLp.addLinkAddress(new LinkAddress("192.0.2.4/24")); + cellLp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("192.0.2.4"), + MOBILE_IFNAME)); + cellLp.addLinkAddress(new LinkAddress("2001:db8:1::1/64")); + cellLp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("2001:db8:1::1"), + MOBILE_IFNAME)); + cellLp.addDnsServer(InetAddress.getByName("2001:db8::1")); + cellLp.addDnsServer(InetAddress.getByName("192.0.2.1")); + + mCellNetworkAgent.sendLinkProperties(cellLp); + mCellNetworkAgent.connect(false); + waitForIdle(); + verify(mMockDnsResolver, times(1)).createNetworkCache( + eq(mCellNetworkAgent.getNetwork().netId)); + verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration( + mResolverParamsParcelCaptor.capture()); + ResolverParamsParcel resolvrParams = mResolverParamsParcelCaptor.getValue(); + assertEquals(2, resolvrParams.tlsServers.length); + assertTrue(ArrayUtils.containsAll(resolvrParams.tlsServers, + new String[] { "2001:db8::1", "192.0.2.1" })); + // Opportunistic mode. + assertEquals(2, resolvrParams.tlsServers.length); + assertTrue(ArrayUtils.containsAll(resolvrParams.tlsServers, + new String[] { "2001:db8::1", "192.0.2.1" })); + reset(mMockDnsResolver); + cellNetworkCallback.expectCallback(CallbackEntry.AVAILABLE, mCellNetworkAgent); + cellNetworkCallback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED, + mCellNetworkAgent); + CallbackEntry.LinkPropertiesChanged cbi = cellNetworkCallback.expectCallback( + CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + cellNetworkCallback.expectCallback(CallbackEntry.BLOCKED_STATUS, mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + assertFalse(cbi.getLp().isPrivateDnsActive()); + assertNull(cbi.getLp().getPrivateDnsServerName()); + + setPrivateDnsSettings(PRIVATE_DNS_MODE_OFF, "ignored.example.com"); + verify(mMockDnsResolver, times(1)).setResolverConfiguration( + mResolverParamsParcelCaptor.capture()); + resolvrParams = mResolverParamsParcelCaptor.getValue(); + assertEquals(2, resolvrParams.servers.length); + assertTrue(ArrayUtils.containsAll(resolvrParams.servers, + new String[] { "2001:db8::1", "192.0.2.1" })); + reset(mMockDnsResolver); + cellNetworkCallback.assertNoCallback(); + + setPrivateDnsSettings(PRIVATE_DNS_MODE_OPPORTUNISTIC, "ignored.example.com"); + verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration( + mResolverParamsParcelCaptor.capture()); + resolvrParams = mResolverParamsParcelCaptor.getValue(); + assertEquals(2, resolvrParams.servers.length); + assertTrue(ArrayUtils.containsAll(resolvrParams.servers, + new String[] { "2001:db8::1", "192.0.2.1" })); + assertEquals(2, resolvrParams.tlsServers.length); + assertTrue(ArrayUtils.containsAll(resolvrParams.tlsServers, + new String[] { "2001:db8::1", "192.0.2.1" })); + reset(mMockDnsResolver); + cellNetworkCallback.assertNoCallback(); + + setPrivateDnsSettings(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME, "strict.example.com"); + // Can't test dns configuration for strict mode without properly mocking + // out the DNS lookups, but can test that LinkProperties is updated. + cbi = cellNetworkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, + mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + assertTrue(cbi.getLp().isPrivateDnsActive()); + assertEquals("strict.example.com", cbi.getLp().getPrivateDnsServerName()); + } + + private PrivateDnsValidationEventParcel makePrivateDnsValidationEvent( + final int netId, final String ipAddress, final String hostname, final int validation) { + final PrivateDnsValidationEventParcel event = new PrivateDnsValidationEventParcel(); + event.netId = netId; + event.ipAddress = ipAddress; + event.hostname = hostname; + event.validation = validation; + return event; + } + + @Test + public void testLinkPropertiesWithPrivateDnsValidationEvents() throws Exception { + // The default on Android is opportunistic mode ("Automatic"). + setPrivateDnsSettings(PRIVATE_DNS_MODE_OPPORTUNISTIC, "ignored.example.com"); + + final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback(); + final NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + mCm.requestNetwork(cellRequest, cellNetworkCallback); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + waitForIdle(); + LinkProperties lp = new LinkProperties(); + mCellNetworkAgent.sendLinkProperties(lp); + mCellNetworkAgent.connect(false); + waitForIdle(); + cellNetworkCallback.expectCallback(CallbackEntry.AVAILABLE, mCellNetworkAgent); + cellNetworkCallback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED, + mCellNetworkAgent); + CallbackEntry.LinkPropertiesChanged cbi = cellNetworkCallback.expectCallback( + CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + cellNetworkCallback.expectCallback(CallbackEntry.BLOCKED_STATUS, mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + assertFalse(cbi.getLp().isPrivateDnsActive()); + assertNull(cbi.getLp().getPrivateDnsServerName()); + Set dnsServers = new HashSet<>(); + checkDnsServers(cbi.getLp(), dnsServers); + + // Send a validation event for a server that is not part of the current + // resolver config. The validation event should be ignored. + mService.mResolverUnsolEventCallback.onPrivateDnsValidationEvent( + makePrivateDnsValidationEvent(mCellNetworkAgent.getNetwork().netId, "", + "145.100.185.18", VALIDATION_RESULT_SUCCESS)); + cellNetworkCallback.assertNoCallback(); + + // Add a dns server to the LinkProperties. + LinkProperties lp2 = new LinkProperties(lp); + lp2.addDnsServer(InetAddress.getByName("145.100.185.16")); + mCellNetworkAgent.sendLinkProperties(lp2); + cbi = cellNetworkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, + mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + assertFalse(cbi.getLp().isPrivateDnsActive()); + assertNull(cbi.getLp().getPrivateDnsServerName()); + dnsServers.add(InetAddress.getByName("145.100.185.16")); + checkDnsServers(cbi.getLp(), dnsServers); + + // Send a validation event containing a hostname that is not part of + // the current resolver config. The validation event should be ignored. + mService.mResolverUnsolEventCallback.onPrivateDnsValidationEvent( + makePrivateDnsValidationEvent(mCellNetworkAgent.getNetwork().netId, + "145.100.185.16", "hostname", VALIDATION_RESULT_SUCCESS)); + cellNetworkCallback.assertNoCallback(); + + // Send a validation event where validation failed. + mService.mResolverUnsolEventCallback.onPrivateDnsValidationEvent( + makePrivateDnsValidationEvent(mCellNetworkAgent.getNetwork().netId, + "145.100.185.16", "", VALIDATION_RESULT_FAILURE)); + cellNetworkCallback.assertNoCallback(); + + // Send a validation event where validation succeeded for a server in + // the current resolver config. A LinkProperties callback with updated + // private dns fields should be sent. + mService.mResolverUnsolEventCallback.onPrivateDnsValidationEvent( + makePrivateDnsValidationEvent(mCellNetworkAgent.getNetwork().netId, + "145.100.185.16", "", VALIDATION_RESULT_SUCCESS)); + cbi = cellNetworkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, + mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + assertTrue(cbi.getLp().isPrivateDnsActive()); + assertNull(cbi.getLp().getPrivateDnsServerName()); + checkDnsServers(cbi.getLp(), dnsServers); + + // The private dns fields in LinkProperties should be preserved when + // the network agent sends unrelated changes. + LinkProperties lp3 = new LinkProperties(lp2); + lp3.setMtu(1300); + mCellNetworkAgent.sendLinkProperties(lp3); + cbi = cellNetworkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, + mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + assertTrue(cbi.getLp().isPrivateDnsActive()); + assertNull(cbi.getLp().getPrivateDnsServerName()); + checkDnsServers(cbi.getLp(), dnsServers); + assertEquals(1300, cbi.getLp().getMtu()); + + // Removing the only validated server should affect the private dns + // fields in LinkProperties. + LinkProperties lp4 = new LinkProperties(lp3); + lp4.removeDnsServer(InetAddress.getByName("145.100.185.16")); + mCellNetworkAgent.sendLinkProperties(lp4); + cbi = cellNetworkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, + mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + assertFalse(cbi.getLp().isPrivateDnsActive()); + assertNull(cbi.getLp().getPrivateDnsServerName()); + dnsServers.remove(InetAddress.getByName("145.100.185.16")); + checkDnsServers(cbi.getLp(), dnsServers); + assertEquals(1300, cbi.getLp().getMtu()); + } + + private void checkDirectlyConnectedRoutes(Object callbackObj, + Collection linkAddresses, Collection otherRoutes) { + assertTrue(callbackObj instanceof LinkProperties); + LinkProperties lp = (LinkProperties) callbackObj; + + Set expectedRoutes = new ArraySet<>(); + expectedRoutes.addAll(otherRoutes); + for (LinkAddress address : linkAddresses) { + RouteInfo localRoute = new RouteInfo(address, null, lp.getInterfaceName()); + // Duplicates in linkAddresses are considered failures + assertTrue(expectedRoutes.add(localRoute)); + } + List observedRoutes = lp.getRoutes(); + assertEquals(expectedRoutes.size(), observedRoutes.size()); + assertTrue(observedRoutes.containsAll(expectedRoutes)); + } + + private static void checkDnsServers(Object callbackObj, Set dnsServers) { + assertTrue(callbackObj instanceof LinkProperties); + LinkProperties lp = (LinkProperties) callbackObj; + assertEquals(dnsServers.size(), lp.getDnsServers().size()); + assertTrue(lp.getDnsServers().containsAll(dnsServers)); + } + + @Test + public void testApplyUnderlyingCapabilities() throws Exception { + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mCellNetworkAgent.connect(false /* validated */); + mWiFiNetworkAgent.connect(false /* validated */); + + final NetworkCapabilities cellNc = new NetworkCapabilities() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_INTERNET) + .addCapability(NET_CAPABILITY_NOT_CONGESTED) + .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED) + .setLinkDownstreamBandwidthKbps(10); + final NetworkCapabilities wifiNc = new NetworkCapabilities() + .addTransportType(TRANSPORT_WIFI) + .addCapability(NET_CAPABILITY_INTERNET) + .addCapability(NET_CAPABILITY_NOT_METERED) + .addCapability(NET_CAPABILITY_NOT_ROAMING) + .addCapability(NET_CAPABILITY_NOT_CONGESTED) + .addCapability(NET_CAPABILITY_NOT_SUSPENDED) + .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED) + .setLinkUpstreamBandwidthKbps(20); + mCellNetworkAgent.setNetworkCapabilities(cellNc, true /* sendToConnectivityService */); + mWiFiNetworkAgent.setNetworkCapabilities(wifiNc, true /* sendToConnectivityService */); + waitForIdle(); + + final Network mobile = mCellNetworkAgent.getNetwork(); + final Network wifi = mWiFiNetworkAgent.getNetwork(); + + final NetworkCapabilities initialCaps = new NetworkCapabilities(); + initialCaps.addTransportType(TRANSPORT_VPN); + initialCaps.addCapability(NET_CAPABILITY_INTERNET); + initialCaps.removeCapability(NET_CAPABILITY_NOT_VPN); + + final NetworkCapabilities withNoUnderlying = new NetworkCapabilities(); + withNoUnderlying.addCapability(NET_CAPABILITY_INTERNET); + withNoUnderlying.addCapability(NET_CAPABILITY_NOT_CONGESTED); + withNoUnderlying.addCapability(NET_CAPABILITY_NOT_ROAMING); + withNoUnderlying.addCapability(NET_CAPABILITY_NOT_SUSPENDED); + withNoUnderlying.addTransportType(TRANSPORT_VPN); + withNoUnderlying.removeCapability(NET_CAPABILITY_NOT_VPN); + + final NetworkCapabilities withMobileUnderlying = new NetworkCapabilities(withNoUnderlying); + withMobileUnderlying.addTransportType(TRANSPORT_CELLULAR); + withMobileUnderlying.removeCapability(NET_CAPABILITY_NOT_ROAMING); + withMobileUnderlying.removeCapability(NET_CAPABILITY_NOT_SUSPENDED); + withMobileUnderlying.setLinkDownstreamBandwidthKbps(10); + + final NetworkCapabilities withWifiUnderlying = new NetworkCapabilities(withNoUnderlying); + withWifiUnderlying.addTransportType(TRANSPORT_WIFI); + withWifiUnderlying.addCapability(NET_CAPABILITY_NOT_METERED); + withWifiUnderlying.setLinkUpstreamBandwidthKbps(20); + + final NetworkCapabilities withWifiAndMobileUnderlying = + new NetworkCapabilities(withNoUnderlying); + withWifiAndMobileUnderlying.addTransportType(TRANSPORT_CELLULAR); + withWifiAndMobileUnderlying.addTransportType(TRANSPORT_WIFI); + withWifiAndMobileUnderlying.removeCapability(NET_CAPABILITY_NOT_METERED); + withWifiAndMobileUnderlying.removeCapability(NET_CAPABILITY_NOT_ROAMING); + withWifiAndMobileUnderlying.setLinkDownstreamBandwidthKbps(10); + withWifiAndMobileUnderlying.setLinkUpstreamBandwidthKbps(20); + + final NetworkCapabilities initialCapsNotMetered = new NetworkCapabilities(initialCaps); + initialCapsNotMetered.addCapability(NET_CAPABILITY_NOT_METERED); + + NetworkCapabilities caps = new NetworkCapabilities(initialCaps); + mService.applyUnderlyingCapabilities(new Network[]{}, initialCapsNotMetered, caps); + assertEquals(withNoUnderlying, caps); + + caps = new NetworkCapabilities(initialCaps); + mService.applyUnderlyingCapabilities(new Network[]{null}, initialCapsNotMetered, caps); + assertEquals(withNoUnderlying, caps); + + caps = new NetworkCapabilities(initialCaps); + mService.applyUnderlyingCapabilities(new Network[]{mobile}, initialCapsNotMetered, caps); + assertEquals(withMobileUnderlying, caps); + + mService.applyUnderlyingCapabilities(new Network[]{wifi}, initialCapsNotMetered, caps); + assertEquals(withWifiUnderlying, caps); + + withWifiUnderlying.removeCapability(NET_CAPABILITY_NOT_METERED); + caps = new NetworkCapabilities(initialCaps); + mService.applyUnderlyingCapabilities(new Network[]{wifi}, initialCaps, caps); + assertEquals(withWifiUnderlying, caps); + + caps = new NetworkCapabilities(initialCaps); + mService.applyUnderlyingCapabilities(new Network[]{mobile, wifi}, initialCaps, caps); + assertEquals(withWifiAndMobileUnderlying, caps); + + withWifiUnderlying.addCapability(NET_CAPABILITY_NOT_METERED); + caps = new NetworkCapabilities(initialCaps); + mService.applyUnderlyingCapabilities(new Network[]{null, mobile, null, wifi}, + initialCapsNotMetered, caps); + assertEquals(withWifiAndMobileUnderlying, caps); + + caps = new NetworkCapabilities(initialCaps); + mService.applyUnderlyingCapabilities(new Network[]{null, mobile, null, wifi}, + initialCapsNotMetered, caps); + assertEquals(withWifiAndMobileUnderlying, caps); + + mService.applyUnderlyingCapabilities(null, initialCapsNotMetered, caps); + assertEquals(withWifiUnderlying, caps); + } + + @Test + public void testVpnConnectDisconnectUnderlyingNetwork() throws Exception { + final TestNetworkCallback callback = new TestNetworkCallback(); + final NetworkRequest request = new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_NOT_VPN).build(); + + mCm.registerNetworkCallback(request, callback); + + // Bring up a VPN that specifies an underlying network that does not exist yet. + // Note: it's sort of meaningless for a VPN app to declare a network that doesn't exist yet, + // (and doing so is difficult without using reflection) but it's good to test that the code + // behaves approximately correctly. + mMockVpn.establishForMyUid(false, true, false); + assertUidRangesUpdatedForMyUid(true); + final Network wifiNetwork = new Network(mNetIdManager.peekNextNetId()); + mMockVpn.setUnderlyingNetworks(new Network[]{wifiNetwork}); + callback.expectAvailableCallbacksUnvalidated(mMockVpn); + assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork()) + .hasTransport(TRANSPORT_VPN)); + assertFalse(mCm.getNetworkCapabilities(mMockVpn.getNetwork()) + .hasTransport(TRANSPORT_WIFI)); + + // Make that underlying network connect, and expect to see its capabilities immediately + // reflected in the VPN's capabilities. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + assertEquals(wifiNetwork, mWiFiNetworkAgent.getNetwork()); + mWiFiNetworkAgent.connect(false); + // TODO: the callback for the VPN happens before any callbacks are called for the wifi + // network that has just connected. There appear to be two issues here: + // 1. The VPN code will accept an underlying network as soon as getNetworkCapabilities() for + // it returns non-null (which happens very early, during handleRegisterNetworkAgent). + // This is not correct because that that point the network is not connected and cannot + // pass any traffic. + // 2. When a network connects, updateNetworkInfo propagates underlying network capabilities + // before rematching networks. + // Given that this scenario can't really happen, this is probably fine for now. + callback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED, mMockVpn); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork()) + .hasTransport(TRANSPORT_VPN)); + assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork()) + .hasTransport(TRANSPORT_WIFI)); + + // Disconnect the network, and expect to see the VPN capabilities change accordingly. + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + callback.expectCapabilitiesThat(mMockVpn, (nc) -> + nc.getTransportTypes().length == 1 && nc.hasTransport(TRANSPORT_VPN)); + + mMockVpn.disconnect(); + mCm.unregisterNetworkCallback(callback); + } + + private void assertGetNetworkInfoOfGetActiveNetworkIsConnected(boolean expectedConnectivity) { + // What Chromium used to do before https://chromium-review.googlesource.com/2605304 + assertEquals("Unexpected result for getActiveNetworkInfo(getActiveNetwork())", + expectedConnectivity, mCm.getNetworkInfo(mCm.getActiveNetwork()).isConnected()); + } + + @Test + public void testVpnUnderlyingNetworkSuspended() throws Exception { + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(callback); + + // Connect a VPN. + mMockVpn.establishForMyUid(false /* validated */, true /* hasInternet */, + false /* isStrictMode */); + callback.expectAvailableCallbacksUnvalidated(mMockVpn); + + // Connect cellular data. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(false /* validated */); + callback.expectCapabilitiesThat(mMockVpn, + nc -> nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED) + && nc.hasTransport(TRANSPORT_CELLULAR)); + callback.assertNoCallback(); + + assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork()) + .hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertGetNetworkInfoOfGetActiveNetworkIsConnected(true); + + // Suspend the cellular network and expect the VPN to be suspended. + mCellNetworkAgent.suspend(); + callback.expectCapabilitiesThat(mMockVpn, + nc -> !nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED) + && nc.hasTransport(TRANSPORT_CELLULAR)); + callback.expectCallback(CallbackEntry.SUSPENDED, mMockVpn); + callback.assertNoCallback(); + + assertFalse(mCm.getNetworkCapabilities(mMockVpn.getNetwork()) + .hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertNetworkInfo(TYPE_MOBILE, DetailedState.SUSPENDED); + assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_VPN, DetailedState.SUSPENDED); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.SUSPENDED); + // VPN's main underlying network is suspended, so no connectivity. + assertGetNetworkInfoOfGetActiveNetworkIsConnected(false); + + // Switch to another network. The VPN should no longer be suspended. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false /* validated */); + callback.expectCapabilitiesThat(mMockVpn, + nc -> nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED) + && nc.hasTransport(TRANSPORT_WIFI)); + callback.expectCallback(CallbackEntry.RESUMED, mMockVpn); + callback.assertNoCallback(); + + assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork()) + .hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertGetNetworkInfoOfGetActiveNetworkIsConnected(true); + + // Unsuspend cellular and then switch back to it. The VPN remains not suspended. + mCellNetworkAgent.resume(); + callback.assertNoCallback(); + mWiFiNetworkAgent.disconnect(); + callback.expectCapabilitiesThat(mMockVpn, + nc -> nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED) + && nc.hasTransport(TRANSPORT_CELLULAR)); + // Spurious double callback? + callback.expectCapabilitiesThat(mMockVpn, + nc -> nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED) + && nc.hasTransport(TRANSPORT_CELLULAR)); + callback.assertNoCallback(); + + assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork()) + .hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertGetNetworkInfoOfGetActiveNetworkIsConnected(true); + + // Suspend cellular and expect no connectivity. + mCellNetworkAgent.suspend(); + callback.expectCapabilitiesThat(mMockVpn, + nc -> !nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED) + && nc.hasTransport(TRANSPORT_CELLULAR)); + callback.expectCallback(CallbackEntry.SUSPENDED, mMockVpn); + callback.assertNoCallback(); + + assertFalse(mCm.getNetworkCapabilities(mMockVpn.getNetwork()) + .hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertNetworkInfo(TYPE_MOBILE, DetailedState.SUSPENDED); + assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_VPN, DetailedState.SUSPENDED); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.SUSPENDED); + assertGetNetworkInfoOfGetActiveNetworkIsConnected(false); + + // Resume cellular and expect that connectivity comes back. + mCellNetworkAgent.resume(); + callback.expectCapabilitiesThat(mMockVpn, + nc -> nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED) + && nc.hasTransport(TRANSPORT_CELLULAR)); + callback.expectCallback(CallbackEntry.RESUMED, mMockVpn); + callback.assertNoCallback(); + + assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork()) + .hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertGetNetworkInfoOfGetActiveNetworkIsConnected(true); + } + + @Test + public void testVpnNetworkActive() throws Exception { + // NETWORK_SETTINGS is necessary to call registerSystemDefaultNetworkCallback. + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + + final int uid = Process.myUid(); + + final TestNetworkCallback genericNetworkCallback = new TestNetworkCallback(); + final TestNetworkCallback genericNotVpnNetworkCallback = new TestNetworkCallback(); + final TestNetworkCallback wifiNetworkCallback = new TestNetworkCallback(); + final TestNetworkCallback vpnNetworkCallback = new TestNetworkCallback(); + final TestNetworkCallback defaultCallback = new TestNetworkCallback(); + final TestNetworkCallback systemDefaultCallback = new TestNetworkCallback(); + final NetworkRequest genericNotVpnRequest = new NetworkRequest.Builder().build(); + final NetworkRequest genericRequest = new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_NOT_VPN).build(); + final NetworkRequest wifiRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).build(); + final NetworkRequest vpnNetworkRequest = new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_NOT_VPN) + .addTransportType(TRANSPORT_VPN).build(); + mCm.registerNetworkCallback(genericRequest, genericNetworkCallback); + mCm.registerNetworkCallback(genericNotVpnRequest, genericNotVpnNetworkCallback); + mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback); + mCm.registerNetworkCallback(vpnNetworkRequest, vpnNetworkCallback); + mCm.registerDefaultNetworkCallback(defaultCallback); + mCm.registerSystemDefaultNetworkCallback(systemDefaultCallback, + new Handler(ConnectivityThread.getInstanceLooper())); + defaultCallback.assertNoCallback(); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + + genericNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + genericNotVpnNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + wifiNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + systemDefaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + vpnNetworkCallback.assertNoCallback(); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + final Set ranges = uidRangesForUids(uid); + mMockVpn.registerAgent(ranges); + mMockVpn.setUnderlyingNetworks(new Network[0]); + + // VPN networks do not satisfy the default request and are automatically validated + // by NetworkMonitor + assertFalse(NetworkMonitorUtils.isValidationRequired( + mMockVpn.getAgent().getNetworkCapabilities())); + mMockVpn.getAgent().setNetworkValid(false /* isStrictMode */); + + mMockVpn.connect(false); + + genericNetworkCallback.expectAvailableThenValidatedCallbacks(mMockVpn); + genericNotVpnNetworkCallback.assertNoCallback(); + wifiNetworkCallback.assertNoCallback(); + vpnNetworkCallback.expectAvailableThenValidatedCallbacks(mMockVpn); + defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn); + systemDefaultCallback.assertNoCallback(); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + assertEquals(mWiFiNetworkAgent.getNetwork(), + systemDefaultCallback.getLastAvailableNetwork()); + + ranges.clear(); + mMockVpn.setUids(ranges); + + genericNetworkCallback.expectCallback(CallbackEntry.LOST, mMockVpn); + genericNotVpnNetworkCallback.assertNoCallback(); + wifiNetworkCallback.assertNoCallback(); + vpnNetworkCallback.expectCallback(CallbackEntry.LOST, mMockVpn); + + // TODO : The default network callback should actually get a LOST call here (also see the + // comment below for AVAILABLE). This is because ConnectivityService does not look at UID + // ranges at all when determining whether a network should be rematched. In practice, VPNs + // can't currently update their UIDs without disconnecting, so this does not matter too + // much, but that is the reason the test here has to check for an update to the + // capabilities instead of the expected LOST then AVAILABLE. + defaultCallback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED, mMockVpn); + systemDefaultCallback.assertNoCallback(); + + ranges.add(new UidRange(uid, uid)); + mMockVpn.setUids(ranges); + + genericNetworkCallback.expectAvailableCallbacksValidated(mMockVpn); + genericNotVpnNetworkCallback.assertNoCallback(); + wifiNetworkCallback.assertNoCallback(); + vpnNetworkCallback.expectAvailableCallbacksValidated(mMockVpn); + // TODO : Here like above, AVAILABLE would be correct, but because this can't actually + // happen outside of the test, ConnectivityService does not rematch callbacks. + defaultCallback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED, mMockVpn); + systemDefaultCallback.assertNoCallback(); + + mWiFiNetworkAgent.disconnect(); + + genericNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + genericNotVpnNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + wifiNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + vpnNetworkCallback.assertNoCallback(); + defaultCallback.assertNoCallback(); + systemDefaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + mMockVpn.disconnect(); + + genericNetworkCallback.expectCallback(CallbackEntry.LOST, mMockVpn); + genericNotVpnNetworkCallback.assertNoCallback(); + wifiNetworkCallback.assertNoCallback(); + vpnNetworkCallback.expectCallback(CallbackEntry.LOST, mMockVpn); + defaultCallback.expectCallback(CallbackEntry.LOST, mMockVpn); + systemDefaultCallback.assertNoCallback(); + assertEquals(null, mCm.getActiveNetwork()); + + mCm.unregisterNetworkCallback(genericNetworkCallback); + mCm.unregisterNetworkCallback(wifiNetworkCallback); + mCm.unregisterNetworkCallback(vpnNetworkCallback); + mCm.unregisterNetworkCallback(defaultCallback); + mCm.unregisterNetworkCallback(systemDefaultCallback); + } + + @Test + public void testVpnWithoutInternet() throws Exception { + final int uid = Process.myUid(); + + final TestNetworkCallback defaultCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(defaultCallback); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + + defaultCallback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + mMockVpn.establishForMyUid(true /* validated */, false /* hasInternet */, + false /* isStrictMode */); + assertUidRangesUpdatedForMyUid(true); + + defaultCallback.assertNoCallback(); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + mMockVpn.disconnect(); + defaultCallback.assertNoCallback(); + + mCm.unregisterNetworkCallback(defaultCallback); + } + + @Test + public void testVpnWithInternet() throws Exception { + final int uid = Process.myUid(); + + final TestNetworkCallback defaultCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(defaultCallback); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + + defaultCallback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + mMockVpn.establishForMyUid(true /* validated */, true /* hasInternet */, + false /* isStrictMode */); + assertUidRangesUpdatedForMyUid(true); + + defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + mMockVpn.disconnect(); + defaultCallback.expectCallback(CallbackEntry.LOST, mMockVpn); + defaultCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent); + + mCm.unregisterNetworkCallback(defaultCallback); + } + + @Test + public void testVpnUnvalidated() throws Exception { + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(callback); + + // Bring up Ethernet. + mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET); + mEthernetNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mEthernetNetworkAgent); + callback.assertNoCallback(); + + // Bring up a VPN that has the INTERNET capability, initially unvalidated. + mMockVpn.establishForMyUid(false /* validated */, true /* hasInternet */, + false /* isStrictMode */); + assertUidRangesUpdatedForMyUid(true); + + // Even though the VPN is unvalidated, it becomes the default network for our app. + callback.expectAvailableCallbacksUnvalidated(mMockVpn); + callback.assertNoCallback(); + + assertTrue(mMockVpn.getAgent().getScore() > mEthernetNetworkAgent.getScore()); + assertEquals(ConnectivityConstants.VPN_DEFAULT_SCORE, mMockVpn.getAgent().getScore()); + assertEquals(mMockVpn.getNetwork(), mCm.getActiveNetwork()); + + NetworkCapabilities nc = mCm.getNetworkCapabilities(mMockVpn.getNetwork()); + assertFalse(nc.hasCapability(NET_CAPABILITY_VALIDATED)); + assertTrue(nc.hasCapability(NET_CAPABILITY_INTERNET)); + + assertFalse(NetworkMonitorUtils.isValidationRequired( + mMockVpn.getAgent().getNetworkCapabilities())); + assertTrue(NetworkMonitorUtils.isPrivateDnsValidationRequired( + mMockVpn.getAgent().getNetworkCapabilities())); + + // Pretend that the VPN network validates. + mMockVpn.getAgent().setNetworkValid(false /* isStrictMode */); + mMockVpn.getAgent().mNetworkMonitor.forceReevaluation(Process.myUid()); + // Expect to see the validated capability, but no other changes, because the VPN is already + // the default network for the app. + callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mMockVpn); + callback.assertNoCallback(); + + mMockVpn.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mMockVpn); + callback.expectAvailableCallbacksValidated(mEthernetNetworkAgent); + } + + @Test + public void testVpnStartsWithUnderlyingCaps() throws Exception { + final int uid = Process.myUid(); + + final TestNetworkCallback vpnNetworkCallback = new TestNetworkCallback(); + final NetworkRequest vpnNetworkRequest = new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_NOT_VPN) + .addTransportType(TRANSPORT_VPN) + .build(); + mCm.registerNetworkCallback(vpnNetworkRequest, vpnNetworkCallback); + vpnNetworkCallback.assertNoCallback(); + + // Connect cell. It will become the default network, and in the absence of setting + // underlying networks explicitly it will become the sole underlying network for the vpn. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.addCapability(NET_CAPABILITY_NOT_SUSPENDED); + mCellNetworkAgent.connect(true); + + mMockVpn.establishForMyUid(true /* validated */, false /* hasInternet */, + false /* isStrictMode */); + assertUidRangesUpdatedForMyUid(true); + + vpnNetworkCallback.expectAvailableCallbacks(mMockVpn.getNetwork(), + false /* suspended */, false /* validated */, false /* blocked */, TIMEOUT_MS); + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn.getNetwork(), TIMEOUT_MS, + nc -> nc.hasCapability(NET_CAPABILITY_VALIDATED)); + + final NetworkCapabilities nc = mCm.getNetworkCapabilities(mMockVpn.getNetwork()); + assertTrue(nc.hasTransport(TRANSPORT_VPN)); + assertTrue(nc.hasTransport(TRANSPORT_CELLULAR)); + assertFalse(nc.hasTransport(TRANSPORT_WIFI)); + assertTrue(nc.hasCapability(NET_CAPABILITY_VALIDATED)); + assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_METERED)); + assertTrue(nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + + assertVpnTransportInfo(nc, VpnManager.TYPE_VPN_SERVICE); + } + + private void assertDefaultNetworkCapabilities(int userId, NetworkAgentWrapper... networks) { + final NetworkCapabilities[] defaultCaps = mService.getDefaultNetworkCapabilitiesForUser( + userId, "com.android.calling.package", "com.test"); + final String defaultCapsString = Arrays.toString(defaultCaps); + assertEquals(defaultCapsString, defaultCaps.length, networks.length); + final Set defaultCapsSet = new ArraySet<>(defaultCaps); + for (NetworkAgentWrapper network : networks) { + final NetworkCapabilities nc = mCm.getNetworkCapabilities(network.getNetwork()); + final String msg = "Did not find " + nc + " in " + Arrays.toString(defaultCaps); + assertTrue(msg, defaultCapsSet.contains(nc)); + } + } + + @Test + public void testVpnSetUnderlyingNetworks() throws Exception { + final TestNetworkCallback vpnNetworkCallback = new TestNetworkCallback(); + final NetworkRequest vpnNetworkRequest = new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_NOT_VPN) + .addTransportType(TRANSPORT_VPN) + .build(); + NetworkCapabilities nc; + mCm.registerNetworkCallback(vpnNetworkRequest, vpnNetworkCallback); + vpnNetworkCallback.assertNoCallback(); + + mMockVpn.establishForMyUid(true /* validated */, false /* hasInternet */, + false /* isStrictMode */); + assertUidRangesUpdatedForMyUid(true); + + vpnNetworkCallback.expectAvailableThenValidatedCallbacks(mMockVpn); + nc = mCm.getNetworkCapabilities(mMockVpn.getNetwork()); + assertTrue(nc.hasTransport(TRANSPORT_VPN)); + assertFalse(nc.hasTransport(TRANSPORT_CELLULAR)); + assertFalse(nc.hasTransport(TRANSPORT_WIFI)); + // For safety reasons a VPN without underlying networks is considered metered. + assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_METERED)); + // A VPN without underlying networks is not suspended. + assertTrue(nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertVpnTransportInfo(nc, VpnManager.TYPE_VPN_SERVICE); + + final int userId = UserHandle.getUserId(Process.myUid()); + assertDefaultNetworkCapabilities(userId /* no networks */); + + // Connect cell and use it as an underlying network. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.addCapability(NET_CAPABILITY_NOT_SUSPENDED); + mCellNetworkAgent.connect(true); + + mMockVpn.setUnderlyingNetworks( + new Network[] { mCellNetworkAgent.getNetwork() }); + + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_CELLULAR) && !caps.hasTransport(TRANSPORT_WIFI) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED) + && caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertDefaultNetworkCapabilities(userId, mCellNetworkAgent); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_SUSPENDED); + mWiFiNetworkAgent.connect(true); + + mMockVpn.setUnderlyingNetworks( + new Network[] { mCellNetworkAgent.getNetwork(), mWiFiNetworkAgent.getNetwork() }); + + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_CELLULAR) && caps.hasTransport(TRANSPORT_WIFI) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED) + && caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertDefaultNetworkCapabilities(userId, mCellNetworkAgent, mWiFiNetworkAgent); + + // Don't disconnect, but note the VPN is not using wifi any more. + mMockVpn.setUnderlyingNetworks( + new Network[] { mCellNetworkAgent.getNetwork() }); + + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_CELLULAR) && !caps.hasTransport(TRANSPORT_WIFI) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED) + && caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + // The return value of getDefaultNetworkCapabilitiesForUser always includes the default + // network (wifi) as well as the underlying networks (cell). + assertDefaultNetworkCapabilities(userId, mCellNetworkAgent, mWiFiNetworkAgent); + + // Remove NOT_SUSPENDED from the only network and observe VPN is now suspended. + mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_SUSPENDED); + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_CELLULAR) && !caps.hasTransport(TRANSPORT_WIFI) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED) + && !caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + vpnNetworkCallback.expectCallback(CallbackEntry.SUSPENDED, mMockVpn); + + // Add NOT_SUSPENDED again and observe VPN is no longer suspended. + mCellNetworkAgent.addCapability(NET_CAPABILITY_NOT_SUSPENDED); + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_CELLULAR) && !caps.hasTransport(TRANSPORT_WIFI) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED) + && caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + vpnNetworkCallback.expectCallback(CallbackEntry.RESUMED, mMockVpn); + + // Use Wifi but not cell. Note the VPN is now unmetered and not suspended. + mMockVpn.setUnderlyingNetworks( + new Network[] { mWiFiNetworkAgent.getNetwork() }); + + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && !caps.hasTransport(TRANSPORT_CELLULAR) && caps.hasTransport(TRANSPORT_WIFI) + && caps.hasCapability(NET_CAPABILITY_NOT_METERED) + && caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertDefaultNetworkCapabilities(userId, mWiFiNetworkAgent); + + // Use both again. + mMockVpn.setUnderlyingNetworks( + new Network[] { mCellNetworkAgent.getNetwork(), mWiFiNetworkAgent.getNetwork() }); + + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_CELLULAR) && caps.hasTransport(TRANSPORT_WIFI) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED) + && caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertDefaultNetworkCapabilities(userId, mCellNetworkAgent, mWiFiNetworkAgent); + + // Cell is suspended again. As WiFi is not, this should not cause a callback. + mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_SUSPENDED); + vpnNetworkCallback.assertNoCallback(); + + // Stop using WiFi. The VPN is suspended again. + mMockVpn.setUnderlyingNetworks( + new Network[] { mCellNetworkAgent.getNetwork() }); + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_CELLULAR) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED) + && !caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + vpnNetworkCallback.expectCallback(CallbackEntry.SUSPENDED, mMockVpn); + assertDefaultNetworkCapabilities(userId, mCellNetworkAgent, mWiFiNetworkAgent); + + // Use both again. + mMockVpn.setUnderlyingNetworks( + new Network[] { mCellNetworkAgent.getNetwork(), mWiFiNetworkAgent.getNetwork() }); + + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_CELLULAR) && caps.hasTransport(TRANSPORT_WIFI) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED) + && caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + vpnNetworkCallback.expectCallback(CallbackEntry.RESUMED, mMockVpn); + assertDefaultNetworkCapabilities(userId, mCellNetworkAgent, mWiFiNetworkAgent); + + // Disconnect cell. Receive update without even removing the dead network from the + // underlying networks – it's dead anyway. Not metered any more. + mCellNetworkAgent.disconnect(); + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && !caps.hasTransport(TRANSPORT_CELLULAR) && caps.hasTransport(TRANSPORT_WIFI) + && caps.hasCapability(NET_CAPABILITY_NOT_METERED)); + assertDefaultNetworkCapabilities(userId, mWiFiNetworkAgent); + + // Disconnect wifi too. No underlying networks means this is now metered. + mWiFiNetworkAgent.disconnect(); + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && !caps.hasTransport(TRANSPORT_CELLULAR) && !caps.hasTransport(TRANSPORT_WIFI) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED)); + // When a network disconnects, the callbacks are fired before all state is updated, so for a + // short time, synchronous calls will behave as if the network is still connected. Wait for + // things to settle. + waitForIdle(); + assertDefaultNetworkCapabilities(userId /* no networks */); + + mMockVpn.disconnect(); + } + + @Test + public void testNullUnderlyingNetworks() throws Exception { + final int uid = Process.myUid(); + + final TestNetworkCallback vpnNetworkCallback = new TestNetworkCallback(); + final NetworkRequest vpnNetworkRequest = new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_NOT_VPN) + .addTransportType(TRANSPORT_VPN) + .build(); + NetworkCapabilities nc; + mCm.registerNetworkCallback(vpnNetworkRequest, vpnNetworkCallback); + vpnNetworkCallback.assertNoCallback(); + + mMockVpn.establishForMyUid(true /* validated */, false /* hasInternet */, + false /* isStrictMode */); + assertUidRangesUpdatedForMyUid(true); + + vpnNetworkCallback.expectAvailableThenValidatedCallbacks(mMockVpn); + nc = mCm.getNetworkCapabilities(mMockVpn.getNetwork()); + assertTrue(nc.hasTransport(TRANSPORT_VPN)); + assertFalse(nc.hasTransport(TRANSPORT_CELLULAR)); + assertFalse(nc.hasTransport(TRANSPORT_WIFI)); + // By default, VPN is set to track default network (i.e. its underlying networks is null). + // In case of no default network, VPN is considered metered. + assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_METERED)); + assertVpnTransportInfo(nc, VpnManager.TYPE_VPN_SERVICE); + + // Connect to Cell; Cell is the default network. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_CELLULAR) && !caps.hasTransport(TRANSPORT_WIFI) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED)); + + // Connect to WiFi; WiFi is the new default. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.connect(true); + + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && !caps.hasTransport(TRANSPORT_CELLULAR) && caps.hasTransport(TRANSPORT_WIFI) + && caps.hasCapability(NET_CAPABILITY_NOT_METERED)); + + // Disconnect Cell. The default network did not change, so there shouldn't be any changes in + // the capabilities. + mCellNetworkAgent.disconnect(); + + // Disconnect wifi too. Now we have no default network. + mWiFiNetworkAgent.disconnect(); + + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && !caps.hasTransport(TRANSPORT_CELLULAR) && !caps.hasTransport(TRANSPORT_WIFI) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED)); + + mMockVpn.disconnect(); + } + + @Test + public void testRestrictedProfileAffectsVpnUidRanges() throws Exception { + // NETWORK_SETTINGS is necessary to see the UID ranges in NetworkCapabilities. + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + + final NetworkRequest request = new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_NOT_VPN) + .build(); + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + // Bring up a VPN + mMockVpn.establishForMyUid(); + assertUidRangesUpdatedForMyUid(true); + callback.expectAvailableThenValidatedCallbacks(mMockVpn); + callback.assertNoCallback(); + + final int uid = Process.myUid(); + NetworkCapabilities nc = mCm.getNetworkCapabilities(mMockVpn.getNetwork()); + assertNotNull("nc=" + nc, nc.getUids()); + assertEquals(nc.getUids(), UidRange.toIntRanges(uidRangesForUids(uid))); + assertVpnTransportInfo(nc, VpnManager.TYPE_VPN_SERVICE); + + // Set an underlying network and expect to see the VPN transports change. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + callback.expectCapabilitiesThat(mMockVpn, (caps) + -> caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_WIFI)); + callback.expectCapabilitiesThat(mWiFiNetworkAgent, (caps) + -> caps.hasCapability(NET_CAPABILITY_VALIDATED)); + + when(mPackageManager.getPackageUidAsUser(ALWAYS_ON_PACKAGE, RESTRICTED_USER)) + .thenReturn(UserHandle.getUid(RESTRICTED_USER, VPN_UID)); + + final Intent addedIntent = new Intent(ACTION_USER_ADDED); + addedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(RESTRICTED_USER)); + addedIntent.putExtra(Intent.EXTRA_USER_HANDLE, RESTRICTED_USER); + + // Send a USER_ADDED broadcast for it. + processBroadcast(addedIntent); + + // Expect that the VPN UID ranges contain both |uid| and the UID range for the newly-added + // restricted user. + final UidRange rRange = UidRange.createForUser(UserHandle.of(RESTRICTED_USER)); + final Range restrictUidRange = new Range(rRange.start, rRange.stop); + final Range singleUidRange = new Range(uid, uid); + callback.expectCapabilitiesThat(mMockVpn, (caps) + -> caps.getUids().size() == 2 + && caps.getUids().contains(singleUidRange) + && caps.getUids().contains(restrictUidRange) + && caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_WIFI)); + + // Change the VPN's capabilities somehow (specifically, disconnect wifi). + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + callback.expectCapabilitiesThat(mMockVpn, (caps) + -> caps.getUids().size() == 2 + && caps.getUids().contains(singleUidRange) + && caps.getUids().contains(restrictUidRange) + && caps.hasTransport(TRANSPORT_VPN) + && !caps.hasTransport(TRANSPORT_WIFI)); + + // Send a USER_REMOVED broadcast and expect to lose the UID range for the restricted user. + final Intent removedIntent = new Intent(ACTION_USER_REMOVED); + removedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(RESTRICTED_USER)); + removedIntent.putExtra(Intent.EXTRA_USER_HANDLE, RESTRICTED_USER); + processBroadcast(removedIntent); + + // Expect that the VPN gains the UID range for the restricted user, and that the capability + // change made just before that (i.e., loss of TRANSPORT_WIFI) is preserved. + callback.expectCapabilitiesThat(mMockVpn, (caps) + -> caps.getUids().size() == 1 + && caps.getUids().contains(singleUidRange) + && caps.hasTransport(TRANSPORT_VPN) + && !caps.hasTransport(TRANSPORT_WIFI)); + } + + @Test + public void testLockdownVpnWithRestrictedProfiles() throws Exception { + // For ConnectivityService#setAlwaysOnVpnPackage. + mServiceContext.setPermission( + Manifest.permission.CONTROL_ALWAYS_ON_VPN, PERMISSION_GRANTED); + // For call Vpn#setAlwaysOnPackage. + mServiceContext.setPermission( + Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED); + // Necessary to see the UID ranges in NetworkCapabilities. + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + + final NetworkRequest request = new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_NOT_VPN) + .build(); + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + final int uid = Process.myUid(); + + // Connect wifi and check that UIDs in the main and restricted profiles have network access. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true /* validated */); + final int restrictedUid = UserHandle.getUid(RESTRICTED_USER, 42 /* appId */); + assertNotNull(mCm.getActiveNetworkForUid(uid)); + assertNotNull(mCm.getActiveNetworkForUid(restrictedUid)); + + // Enable always-on VPN lockdown. The main user loses network access because no VPN is up. + final ArrayList allowList = new ArrayList<>(); + mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, ALWAYS_ON_PACKAGE, + true /* lockdown */, allowList); + waitForIdle(); + assertNull(mCm.getActiveNetworkForUid(uid)); + // This is arguably overspecified: a UID that is not running doesn't have an active network. + // But it's useful to check that non-default users do not lose network access, and to prove + // that the loss of connectivity below is indeed due to the restricted profile coming up. + assertNotNull(mCm.getActiveNetworkForUid(restrictedUid)); + + // Start the restricted profile, and check that the UID within it loses network access. + when(mPackageManager.getPackageUidAsUser(ALWAYS_ON_PACKAGE, RESTRICTED_USER)) + .thenReturn(UserHandle.getUid(RESTRICTED_USER, VPN_UID)); + when(mUserManager.getAliveUsers()).thenReturn(Arrays.asList(PRIMARY_USER_INFO, + RESTRICTED_USER_INFO)); + // TODO: check that VPN app within restricted profile still has access, etc. + final Intent addedIntent = new Intent(ACTION_USER_ADDED); + addedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(RESTRICTED_USER)); + addedIntent.putExtra(Intent.EXTRA_USER_HANDLE, RESTRICTED_USER); + processBroadcast(addedIntent); + assertNull(mCm.getActiveNetworkForUid(uid)); + assertNull(mCm.getActiveNetworkForUid(restrictedUid)); + + // Stop the restricted profile, and check that the UID within it has network access again. + when(mUserManager.getAliveUsers()).thenReturn(Arrays.asList(PRIMARY_USER_INFO)); + + // Send a USER_REMOVED broadcast and expect to lose the UID range for the restricted user. + final Intent removedIntent = new Intent(ACTION_USER_REMOVED); + removedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(RESTRICTED_USER)); + removedIntent.putExtra(Intent.EXTRA_USER_HANDLE, RESTRICTED_USER); + processBroadcast(removedIntent); + assertNull(mCm.getActiveNetworkForUid(uid)); + assertNotNull(mCm.getActiveNetworkForUid(restrictedUid)); + + mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, null, false /* lockdown */, + allowList); + waitForIdle(); + } + + @Test + public void testIsActiveNetworkMeteredOverWifi() throws Exception { + // Returns true by default when no network is available. + assertTrue(mCm.isActiveNetworkMetered()); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.connect(true); + waitForIdle(); + + assertFalse(mCm.isActiveNetworkMetered()); + } + + @Test + public void testIsActiveNetworkMeteredOverCell() throws Exception { + // Returns true by default when no network is available. + assertTrue(mCm.isActiveNetworkMetered()); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED); + mCellNetworkAgent.connect(true); + waitForIdle(); + + assertTrue(mCm.isActiveNetworkMetered()); + } + + @Test + public void testIsActiveNetworkMeteredOverVpnTrackingPlatformDefault() throws Exception { + // Returns true by default when no network is available. + assertTrue(mCm.isActiveNetworkMetered()); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED); + mCellNetworkAgent.connect(true); + waitForIdle(); + assertTrue(mCm.isActiveNetworkMetered()); + + // Connect VPN network. By default it is using current default network (Cell). + mMockVpn.establishForMyUid(); + assertUidRangesUpdatedForMyUid(true); + + // Ensure VPN is now the active network. + assertEquals(mMockVpn.getNetwork(), mCm.getActiveNetwork()); + + // Expect VPN to be metered. + assertTrue(mCm.isActiveNetworkMetered()); + + // Connect WiFi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.connect(true); + waitForIdle(); + // VPN should still be the active network. + assertEquals(mMockVpn.getNetwork(), mCm.getActiveNetwork()); + + // Expect VPN to be unmetered as it should now be using WiFi (new default). + assertFalse(mCm.isActiveNetworkMetered()); + + // Disconnecting Cell should not affect VPN's meteredness. + mCellNetworkAgent.disconnect(); + waitForIdle(); + + assertFalse(mCm.isActiveNetworkMetered()); + + // Disconnect WiFi; Now there is no platform default network. + mWiFiNetworkAgent.disconnect(); + waitForIdle(); + + // VPN without any underlying networks is treated as metered. + assertTrue(mCm.isActiveNetworkMetered()); + + mMockVpn.disconnect(); + } + + @Test + public void testIsActiveNetworkMeteredOverVpnSpecifyingUnderlyingNetworks() throws Exception { + // Returns true by default when no network is available. + assertTrue(mCm.isActiveNetworkMetered()); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED); + mCellNetworkAgent.connect(true); + waitForIdle(); + assertTrue(mCm.isActiveNetworkMetered()); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.connect(true); + waitForIdle(); + assertFalse(mCm.isActiveNetworkMetered()); + + // Connect VPN network. + mMockVpn.establishForMyUid(); + assertUidRangesUpdatedForMyUid(true); + + // Ensure VPN is now the active network. + assertEquals(mMockVpn.getNetwork(), mCm.getActiveNetwork()); + // VPN is using Cell + mMockVpn.setUnderlyingNetworks( + new Network[] { mCellNetworkAgent.getNetwork() }); + waitForIdle(); + + // Expect VPN to be metered. + assertTrue(mCm.isActiveNetworkMetered()); + + // VPN is now using WiFi + mMockVpn.setUnderlyingNetworks( + new Network[] { mWiFiNetworkAgent.getNetwork() }); + waitForIdle(); + + // Expect VPN to be unmetered + assertFalse(mCm.isActiveNetworkMetered()); + + // VPN is using Cell | WiFi. + mMockVpn.setUnderlyingNetworks( + new Network[] { mCellNetworkAgent.getNetwork(), mWiFiNetworkAgent.getNetwork() }); + waitForIdle(); + + // Expect VPN to be metered. + assertTrue(mCm.isActiveNetworkMetered()); + + // VPN is using WiFi | Cell. + mMockVpn.setUnderlyingNetworks( + new Network[] { mWiFiNetworkAgent.getNetwork(), mCellNetworkAgent.getNetwork() }); + waitForIdle(); + + // Order should not matter and VPN should still be metered. + assertTrue(mCm.isActiveNetworkMetered()); + + // VPN is not using any underlying networks. + mMockVpn.setUnderlyingNetworks(new Network[0]); + waitForIdle(); + + // VPN without underlying networks is treated as metered. + assertTrue(mCm.isActiveNetworkMetered()); + + mMockVpn.disconnect(); + } + + @Test + public void testIsActiveNetworkMeteredOverAlwaysMeteredVpn() throws Exception { + // Returns true by default when no network is available. + assertTrue(mCm.isActiveNetworkMetered()); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.connect(true); + waitForIdle(); + assertFalse(mCm.isActiveNetworkMetered()); + + // Connect VPN network. + mMockVpn.registerAgent(true /* isAlwaysMetered */, uidRangesForUids(Process.myUid()), + new LinkProperties()); + mMockVpn.connect(true); + waitForIdle(); + assertEquals(mMockVpn.getNetwork(), mCm.getActiveNetwork()); + + // VPN is tracking current platform default (WiFi). + mMockVpn.setUnderlyingNetworks(null); + waitForIdle(); + + // Despite VPN using WiFi (which is unmetered), VPN itself is marked as always metered. + assertTrue(mCm.isActiveNetworkMetered()); + + + // VPN explicitly declares WiFi as its underlying network. + mMockVpn.setUnderlyingNetworks( + new Network[] { mWiFiNetworkAgent.getNetwork() }); + waitForIdle(); + + // Doesn't really matter whether VPN declares its underlying networks explicitly. + assertTrue(mCm.isActiveNetworkMetered()); + + // With WiFi lost, VPN is basically without any underlying networks. And in that case it is + // anyways suppose to be metered. + mWiFiNetworkAgent.disconnect(); + waitForIdle(); + + assertTrue(mCm.isActiveNetworkMetered()); + + mMockVpn.disconnect(); + } + + private class DetailedBlockedStatusCallback extends TestNetworkCallback { + public void expectAvailableThenValidatedCallbacks(HasNetwork n, int blockedStatus) { + super.expectAvailableThenValidatedCallbacks(n.getNetwork(), blockedStatus, TIMEOUT_MS); + } + public void expectBlockedStatusCallback(HasNetwork n, int blockedStatus) { + // This doesn't work: + // super.expectBlockedStatusCallback(blockedStatus, n.getNetwork()); + super.expectBlockedStatusCallback(blockedStatus, n.getNetwork(), TIMEOUT_MS); + } + public void onBlockedStatusChanged(Network network, int blockedReasons) { + getHistory().add(new CallbackEntry.BlockedStatusInt(network, blockedReasons)); + } + } + + @Test + public void testNetworkBlockedStatus() throws Exception { + final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback(); + final NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .build(); + mCm.registerNetworkCallback(cellRequest, cellNetworkCallback); + final DetailedBlockedStatusCallback detailedCallback = new DetailedBlockedStatusCallback(); + mCm.registerNetworkCallback(cellRequest, detailedCallback); + + mockUidNetworkingBlocked(); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + detailedCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent, + BLOCKED_REASON_NONE); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertExtraInfoFromCmPresent(mCellNetworkAgent); + + setBlockedReasonChanged(BLOCKED_REASON_BATTERY_SAVER); + cellNetworkCallback.expectBlockedStatusCallback(true, mCellNetworkAgent); + detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, + BLOCKED_REASON_BATTERY_SAVER); + assertNull(mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertExtraInfoFromCmBlocked(mCellNetworkAgent); + + // If blocked state does not change but blocked reason does, the boolean callback is called. + // TODO: investigate de-duplicating. + setBlockedReasonChanged(BLOCKED_METERED_REASON_USER_RESTRICTED); + cellNetworkCallback.expectBlockedStatusCallback(true, mCellNetworkAgent); + detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, + BLOCKED_METERED_REASON_USER_RESTRICTED); + + setBlockedReasonChanged(BLOCKED_REASON_NONE); + cellNetworkCallback.expectBlockedStatusCallback(false, mCellNetworkAgent); + detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, BLOCKED_REASON_NONE); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertExtraInfoFromCmPresent(mCellNetworkAgent); + + setBlockedReasonChanged(BLOCKED_METERED_REASON_DATA_SAVER); + cellNetworkCallback.expectBlockedStatusCallback(true, mCellNetworkAgent); + detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, + BLOCKED_METERED_REASON_DATA_SAVER); + assertNull(mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertExtraInfoFromCmBlocked(mCellNetworkAgent); + + // Restrict the network based on UID rule and NOT_METERED capability change. + mCellNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + cellNetworkCallback.expectCapabilitiesWith(NET_CAPABILITY_NOT_METERED, mCellNetworkAgent); + cellNetworkCallback.expectBlockedStatusCallback(false, mCellNetworkAgent); + detailedCallback.expectCapabilitiesWith(NET_CAPABILITY_NOT_METERED, mCellNetworkAgent); + detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, BLOCKED_REASON_NONE); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertExtraInfoFromCmPresent(mCellNetworkAgent); + + mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED); + cellNetworkCallback.expectCapabilitiesWithout(NET_CAPABILITY_NOT_METERED, + mCellNetworkAgent); + cellNetworkCallback.expectBlockedStatusCallback(true, mCellNetworkAgent); + detailedCallback.expectCapabilitiesWithout(NET_CAPABILITY_NOT_METERED, + mCellNetworkAgent); + detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, + BLOCKED_METERED_REASON_DATA_SAVER); + assertNull(mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertExtraInfoFromCmBlocked(mCellNetworkAgent); + + setBlockedReasonChanged(BLOCKED_REASON_NONE); + cellNetworkCallback.expectBlockedStatusCallback(false, mCellNetworkAgent); + detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, BLOCKED_REASON_NONE); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertExtraInfoFromCmPresent(mCellNetworkAgent); + + setBlockedReasonChanged(BLOCKED_REASON_NONE); + cellNetworkCallback.assertNoCallback(); + detailedCallback.assertNoCallback(); + + // Restrict background data. Networking is not blocked because the network is unmetered. + setBlockedReasonChanged(BLOCKED_METERED_REASON_DATA_SAVER); + cellNetworkCallback.expectBlockedStatusCallback(true, mCellNetworkAgent); + detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, + BLOCKED_METERED_REASON_DATA_SAVER); + assertNull(mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertExtraInfoFromCmBlocked(mCellNetworkAgent); + setBlockedReasonChanged(BLOCKED_METERED_REASON_DATA_SAVER); + cellNetworkCallback.assertNoCallback(); + + setBlockedReasonChanged(BLOCKED_REASON_NONE); + cellNetworkCallback.expectBlockedStatusCallback(false, mCellNetworkAgent); + detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, BLOCKED_REASON_NONE); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertExtraInfoFromCmPresent(mCellNetworkAgent); + + setBlockedReasonChanged(BLOCKED_REASON_NONE); + cellNetworkCallback.assertNoCallback(); + detailedCallback.assertNoCallback(); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertExtraInfoFromCmPresent(mCellNetworkAgent); + + mCm.unregisterNetworkCallback(cellNetworkCallback); + } + + @Test + public void testNetworkBlockedStatusBeforeAndAfterConnect() throws Exception { + final TestNetworkCallback defaultCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(defaultCallback); + mockUidNetworkingBlocked(); + + // No Networkcallbacks invoked before any network is active. + setBlockedReasonChanged(BLOCKED_REASON_BATTERY_SAVER); + setBlockedReasonChanged(BLOCKED_REASON_NONE); + setBlockedReasonChanged(BLOCKED_METERED_REASON_DATA_SAVER); + defaultCallback.assertNoCallback(); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellNetworkAgent); + defaultCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mCellNetworkAgent); + + // Allow to use the network after switching to NOT_METERED network. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.connect(true); + defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + + // Switch to METERED network. Restrict the use of the network. + mWiFiNetworkAgent.disconnect(); + defaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + defaultCallback.expectAvailableCallbacksValidatedAndBlocked(mCellNetworkAgent); + + // Network becomes NOT_METERED. + mCellNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + defaultCallback.expectCapabilitiesWith(NET_CAPABILITY_NOT_METERED, mCellNetworkAgent); + defaultCallback.expectBlockedStatusCallback(false, mCellNetworkAgent); + + // Verify there's no Networkcallbacks invoked after data saver on/off. + setBlockedReasonChanged(BLOCKED_METERED_REASON_DATA_SAVER); + setBlockedReasonChanged(BLOCKED_REASON_NONE); + defaultCallback.assertNoCallback(); + + mCellNetworkAgent.disconnect(); + defaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + defaultCallback.assertNoCallback(); + + mCm.unregisterNetworkCallback(defaultCallback); + } + + private void expectNetworkRejectNonSecureVpn(InOrder inOrder, boolean add, + UidRangeParcel... expected) throws Exception { + inOrder.verify(mMockNetd).networkRejectNonSecureVpn(eq(add), aryEq(expected)); + } + + private void checkNetworkInfo(NetworkInfo ni, int type, DetailedState state) { + assertNotNull(ni); + assertEquals(type, ni.getType()); + assertEquals(ConnectivityManager.getNetworkTypeName(type), state, ni.getDetailedState()); + if (state == DetailedState.CONNECTED || state == DetailedState.SUSPENDED) { + assertNotNull(ni.getExtraInfo()); + } else { + // Technically speaking, a network that's in CONNECTING state will generally have a + // non-null extraInfo. This doesn't actually happen in this test because it never calls + // a legacy API while a network is connecting. When a network is in CONNECTING state + // because of legacy lockdown VPN, its extraInfo is always null. + assertNull(ni.getExtraInfo()); + } + } + + private void assertActiveNetworkInfo(int type, DetailedState state) { + checkNetworkInfo(mCm.getActiveNetworkInfo(), type, state); + } + private void assertNetworkInfo(int type, DetailedState state) { + checkNetworkInfo(mCm.getNetworkInfo(type), type, state); + } + + private void assertExtraInfoFromCm(TestNetworkAgentWrapper network, boolean present) { + final NetworkInfo niForNetwork = mCm.getNetworkInfo(network.getNetwork()); + final NetworkInfo niForType = mCm.getNetworkInfo(network.getLegacyType()); + if (present) { + assertEquals(network.getExtraInfo(), niForNetwork.getExtraInfo()); + assertEquals(network.getExtraInfo(), niForType.getExtraInfo()); + } else { + assertNull(niForNetwork.getExtraInfo()); + assertNull(niForType.getExtraInfo()); + } + } + + private void assertExtraInfoFromCmBlocked(TestNetworkAgentWrapper network) { + assertExtraInfoFromCm(network, false); + } + + private void assertExtraInfoFromCmPresent(TestNetworkAgentWrapper network) { + assertExtraInfoFromCm(network, true); + } + + // Checks that each of the |agents| receive a blocked status change callback with the specified + // |blocked| value, in any order. This is needed because when an event affects multiple + // networks, ConnectivityService does not guarantee the order in which callbacks are fired. + private void assertBlockedCallbackInAnyOrder(TestNetworkCallback callback, boolean blocked, + TestNetworkAgentWrapper... agents) { + final List expectedNetworks = Arrays.asList(agents).stream() + .map((agent) -> agent.getNetwork()) + .collect(Collectors.toList()); + + // Expect exactly one blocked callback for each agent. + for (int i = 0; i < agents.length; i++) { + CallbackEntry e = callback.expectCallbackThat(TIMEOUT_MS, (c) -> + c instanceof CallbackEntry.BlockedStatus + && ((CallbackEntry.BlockedStatus) c).getBlocked() == blocked); + Network network = e.getNetwork(); + assertTrue("Received unexpected blocked callback for network " + network, + expectedNetworks.remove(network)); + } + } + + @Test + public void testNetworkBlockedStatusAlwaysOnVpn() throws Exception { + mServiceContext.setPermission( + Manifest.permission.CONTROL_ALWAYS_ON_VPN, PERMISSION_GRANTED); + mServiceContext.setPermission( + Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED); + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + + final TestNetworkCallback callback = new TestNetworkCallback(); + final NetworkRequest request = new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_NOT_VPN) + .build(); + mCm.registerNetworkCallback(request, callback); + + final TestNetworkCallback defaultCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(defaultCallback); + + final TestNetworkCallback vpnUidCallback = new TestNetworkCallback(); + final NetworkRequest vpnUidRequest = new NetworkRequest.Builder().build(); + registerNetworkCallbackAsUid(vpnUidRequest, vpnUidCallback, VPN_UID); + + final TestNetworkCallback vpnUidDefaultCallback = new TestNetworkCallback(); + registerDefaultNetworkCallbackAsUid(vpnUidDefaultCallback, VPN_UID); + + final TestNetworkCallback vpnDefaultCallbackAsUid = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallbackForUid(VPN_UID, vpnDefaultCallbackAsUid, + new Handler(ConnectivityThread.getInstanceLooper())); + + final int uid = Process.myUid(); + final int userId = UserHandle.getUserId(uid); + final ArrayList allowList = new ArrayList<>(); + mVpnManagerService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, true /* lockdown */, + allowList); + waitForIdle(); + + UidRangeParcel firstHalf = new UidRangeParcel(1, VPN_UID - 1); + UidRangeParcel secondHalf = new UidRangeParcel(VPN_UID + 1, 99999); + InOrder inOrder = inOrder(mMockNetd); + expectNetworkRejectNonSecureVpn(inOrder, true, firstHalf, secondHalf); + + // Connect a network when lockdown is active, expect to see it blocked. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false /* validated */); + callback.expectAvailableCallbacksUnvalidatedAndBlocked(mWiFiNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mWiFiNetworkAgent); + vpnUidCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + vpnUidDefaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + vpnDefaultCallbackAsUid.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID)); + assertNull(mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED); + // Mobile is BLOCKED even though it's not actually connected. + assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED); + + // Disable lockdown, expect to see the network unblocked. + mVpnManagerService.setAlwaysOnVpnPackage(userId, null, false /* lockdown */, allowList); + callback.expectBlockedStatusCallback(false, mWiFiNetworkAgent); + defaultCallback.expectBlockedStatusCallback(false, mWiFiNetworkAgent); + vpnUidCallback.assertNoCallback(); + vpnUidDefaultCallback.assertNoCallback(); + vpnDefaultCallbackAsUid.assertNoCallback(); + expectNetworkRejectNonSecureVpn(inOrder, false, firstHalf, secondHalf); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID)); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + + // Add our UID to the allowlist and re-enable lockdown, expect network is not blocked. + allowList.add(TEST_PACKAGE_NAME); + mVpnManagerService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, true /* lockdown */, + allowList); + callback.assertNoCallback(); + defaultCallback.assertNoCallback(); + vpnUidCallback.assertNoCallback(); + vpnUidDefaultCallback.assertNoCallback(); + vpnDefaultCallbackAsUid.assertNoCallback(); + + // The following requires that the UID of this test package is greater than VPN_UID. This + // is always true in practice because a plain AOSP build with no apps installed has almost + // 200 packages installed. + final UidRangeParcel piece1 = new UidRangeParcel(1, VPN_UID - 1); + final UidRangeParcel piece2 = new UidRangeParcel(VPN_UID + 1, uid - 1); + final UidRangeParcel piece3 = new UidRangeParcel(uid + 1, 99999); + expectNetworkRejectNonSecureVpn(inOrder, true, piece1, piece2, piece3); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID)); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + + // Connect a new network, expect it to be unblocked. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(false /* validated */); + callback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + defaultCallback.assertNoCallback(); + vpnUidCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + vpnUidDefaultCallback.assertNoCallback(); + vpnDefaultCallbackAsUid.assertNoCallback(); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID)); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + // Cellular is DISCONNECTED because it's not the default and there are no requests for it. + assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + + // Disable lockdown, remove our UID from the allowlist, and re-enable lockdown. + // Everything should now be blocked. + mVpnManagerService.setAlwaysOnVpnPackage(userId, null, false /* lockdown */, allowList); + waitForIdle(); + expectNetworkRejectNonSecureVpn(inOrder, false, piece1, piece2, piece3); + allowList.clear(); + mVpnManagerService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, true /* lockdown */, + allowList); + waitForIdle(); + expectNetworkRejectNonSecureVpn(inOrder, true, firstHalf, secondHalf); + defaultCallback.expectBlockedStatusCallback(true, mWiFiNetworkAgent); + assertBlockedCallbackInAnyOrder(callback, true, mWiFiNetworkAgent, mCellNetworkAgent); + vpnUidCallback.assertNoCallback(); + vpnUidDefaultCallback.assertNoCallback(); + vpnDefaultCallbackAsUid.assertNoCallback(); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID)); + assertNull(mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED); + + // Disable lockdown. Everything is unblocked. + mVpnManagerService.setAlwaysOnVpnPackage(userId, null, false /* lockdown */, allowList); + defaultCallback.expectBlockedStatusCallback(false, mWiFiNetworkAgent); + assertBlockedCallbackInAnyOrder(callback, false, mWiFiNetworkAgent, mCellNetworkAgent); + vpnUidCallback.assertNoCallback(); + vpnUidDefaultCallback.assertNoCallback(); + vpnDefaultCallbackAsUid.assertNoCallback(); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID)); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + + // Enable and disable an always-on VPN package without lockdown. Expect no changes. + reset(mMockNetd); + mVpnManagerService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, false /* lockdown */, + allowList); + inOrder.verify(mMockNetd, never()).networkRejectNonSecureVpn(anyBoolean(), any()); + callback.assertNoCallback(); + defaultCallback.assertNoCallback(); + vpnUidCallback.assertNoCallback(); + vpnUidDefaultCallback.assertNoCallback(); + vpnDefaultCallbackAsUid.assertNoCallback(); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID)); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + + mVpnManagerService.setAlwaysOnVpnPackage(userId, null, false /* lockdown */, allowList); + inOrder.verify(mMockNetd, never()).networkRejectNonSecureVpn(anyBoolean(), any()); + callback.assertNoCallback(); + defaultCallback.assertNoCallback(); + vpnUidCallback.assertNoCallback(); + vpnUidDefaultCallback.assertNoCallback(); + vpnDefaultCallbackAsUid.assertNoCallback(); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID)); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + + // Enable lockdown and connect a VPN. The VPN is not blocked. + mVpnManagerService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, true /* lockdown */, + allowList); + defaultCallback.expectBlockedStatusCallback(true, mWiFiNetworkAgent); + assertBlockedCallbackInAnyOrder(callback, true, mWiFiNetworkAgent, mCellNetworkAgent); + vpnUidCallback.assertNoCallback(); + vpnUidDefaultCallback.assertNoCallback(); + vpnDefaultCallbackAsUid.assertNoCallback(); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID)); + assertNull(mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED); + + mMockVpn.establishForMyUid(); + assertUidRangesUpdatedForMyUid(true); + defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn); + vpnUidCallback.assertNoCallback(); // vpnUidCallback has NOT_VPN capability. + vpnUidDefaultCallback.assertNoCallback(); // VPN does not apply to VPN_UID + vpnDefaultCallbackAsUid.assertNoCallback(); + assertEquals(mMockVpn.getNetwork(), mCm.getActiveNetwork()); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID)); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + + mMockVpn.disconnect(); + defaultCallback.expectCallback(CallbackEntry.LOST, mMockVpn); + defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mWiFiNetworkAgent); + vpnUidCallback.assertNoCallback(); + vpnUidDefaultCallback.assertNoCallback(); + vpnDefaultCallbackAsUid.assertNoCallback(); + assertNull(mCm.getActiveNetwork()); + + mCm.unregisterNetworkCallback(callback); + mCm.unregisterNetworkCallback(defaultCallback); + mCm.unregisterNetworkCallback(vpnUidCallback); + mCm.unregisterNetworkCallback(vpnUidDefaultCallback); + mCm.unregisterNetworkCallback(vpnDefaultCallbackAsUid); + } + + private void setupLegacyLockdownVpn() { + final String profileName = "testVpnProfile"; + final byte[] profileTag = profileName.getBytes(StandardCharsets.UTF_8); + when(mVpnProfileStore.get(Credentials.LOCKDOWN_VPN)).thenReturn(profileTag); + + final VpnProfile profile = new VpnProfile(profileName); + profile.name = "My VPN"; + profile.server = "192.0.2.1"; + profile.dnsServers = "8.8.8.8"; + profile.type = VpnProfile.TYPE_IPSEC_XAUTH_PSK; + final byte[] encodedProfile = profile.encode(); + when(mVpnProfileStore.get(Credentials.VPN + profileName)).thenReturn(encodedProfile); + } + + private void establishLegacyLockdownVpn(Network underlying) throws Exception { + // The legacy lockdown VPN only supports userId 0, and must have an underlying network. + assertNotNull(underlying); + mMockVpn.setVpnType(VpnManager.TYPE_VPN_LEGACY); + // The legacy lockdown VPN only supports userId 0. + final Set ranges = Collections.singleton(PRIMARY_UIDRANGE); + mMockVpn.registerAgent(ranges); + mMockVpn.setUnderlyingNetworks(new Network[]{underlying}); + mMockVpn.connect(true); + } + + @Test + public void testLegacyLockdownVpn() throws Exception { + mServiceContext.setPermission( + Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED); + // For LockdownVpnTracker to call registerSystemDefaultNetworkCallback. + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + + final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build(); + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + final TestNetworkCallback defaultCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(defaultCallback); + + final TestNetworkCallback systemDefaultCallback = new TestNetworkCallback(); + mCm.registerSystemDefaultNetworkCallback(systemDefaultCallback, + new Handler(ConnectivityThread.getInstanceLooper())); + + // Pretend lockdown VPN was configured. + setupLegacyLockdownVpn(); + + // LockdownVpnTracker disables the Vpn teardown code and enables lockdown. + // Check the VPN's state before it does so. + assertTrue(mMockVpn.getEnableTeardown()); + assertFalse(mMockVpn.getLockdown()); + + // Send a USER_UNLOCKED broadcast so CS starts LockdownVpnTracker. + final int userId = UserHandle.getUserId(Process.myUid()); + final Intent addedIntent = new Intent(ACTION_USER_UNLOCKED); + addedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(userId)); + addedIntent.putExtra(Intent.EXTRA_USER_HANDLE, userId); + processBroadcast(addedIntent); + + // Lockdown VPN disables teardown and enables lockdown. + assertFalse(mMockVpn.getEnableTeardown()); + assertTrue(mMockVpn.getLockdown()); + + // Bring up a network. + // Expect nothing to happen because the network does not have an IPv4 default route: legacy + // VPN only supports IPv4. + final LinkProperties cellLp = new LinkProperties(); + cellLp.setInterfaceName("rmnet0"); + cellLp.addLinkAddress(new LinkAddress("2001:db8::1/64")); + cellLp.addRoute(new RouteInfo(new IpPrefix("::/0"), null, "rmnet0")); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp); + mCellNetworkAgent.connect(false /* validated */); + callback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellNetworkAgent); + systemDefaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellNetworkAgent); + waitForIdle(); + assertNull(mMockVpn.getAgent()); + + // Add an IPv4 address. Ideally the VPN should start, but it doesn't because nothing calls + // LockdownVpnTracker#handleStateChangedLocked. This is a bug. + // TODO: consider fixing this. + cellLp.addLinkAddress(new LinkAddress("192.0.2.2/25")); + cellLp.addRoute(new RouteInfo(new IpPrefix("0.0.0.0/0"), null, "rmnet0")); + mCellNetworkAgent.sendLinkProperties(cellLp); + callback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + defaultCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + systemDefaultCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, + mCellNetworkAgent); + waitForIdle(); + assertNull(mMockVpn.getAgent()); + + // Disconnect, then try again with a network that supports IPv4 at connection time. + // Expect lockdown VPN to come up. + ExpectedBroadcast b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.DISCONNECTED); + mCellNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + defaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + systemDefaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + b1.expectBroadcast(); + + // When lockdown VPN is active, the NetworkInfo state in CONNECTIVITY_ACTION is overwritten + // with the state of the VPN network. So expect a CONNECTING broadcast. + b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTING); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp); + mCellNetworkAgent.connect(false /* validated */); + callback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellNetworkAgent); + systemDefaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellNetworkAgent); + b1.expectBroadcast(); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_VPN, DetailedState.BLOCKED); + assertExtraInfoFromCmBlocked(mCellNetworkAgent); + + // TODO: it would be nice if we could simply rely on the production code here, and have + // LockdownVpnTracker start the VPN, have the VPN code register its NetworkAgent with + // ConnectivityService, etc. That would require duplicating a fair bit of code from the + // Vpn tests around how to mock out LegacyVpnRunner. But even if we did that, this does not + // work for at least two reasons: + // 1. In this test, calling registerNetworkAgent does not actually result in an agent being + // registered. This is because nothing calls onNetworkMonitorCreated, which is what + // actually ends up causing handleRegisterNetworkAgent to be called. Code in this test + // that wants to register an agent must use TestNetworkAgentWrapper. + // 2. Even if we exposed Vpn#agentConnect to the test, and made MockVpn#agentConnect call + // the TestNetworkAgentWrapper code, this would deadlock because the + // TestNetworkAgentWrapper code cannot be called on the handler thread since it calls + // waitForIdle(). + mMockVpn.expectStartLegacyVpnRunner(); + b1 = expectConnectivityAction(TYPE_VPN, DetailedState.CONNECTED); + ExpectedBroadcast b2 = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTED); + establishLegacyLockdownVpn(mCellNetworkAgent.getNetwork()); + callback.expectAvailableThenValidatedCallbacks(mMockVpn); + defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn); + systemDefaultCallback.assertNoCallback(); + NetworkCapabilities vpnNc = mCm.getNetworkCapabilities(mMockVpn.getNetwork()); + b1.expectBroadcast(); + b2.expectBroadcast(); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED); + assertExtraInfoFromCmPresent(mCellNetworkAgent); + assertTrue(vpnNc.hasTransport(TRANSPORT_VPN)); + assertTrue(vpnNc.hasTransport(TRANSPORT_CELLULAR)); + assertFalse(vpnNc.hasTransport(TRANSPORT_WIFI)); + assertFalse(vpnNc.hasCapability(NET_CAPABILITY_NOT_METERED)); + assertVpnTransportInfo(vpnNc, VpnManager.TYPE_VPN_LEGACY); + + // Switch default network from cell to wifi. Expect VPN to disconnect and reconnect. + final LinkProperties wifiLp = new LinkProperties(); + wifiLp.setInterfaceName("wlan0"); + wifiLp.addLinkAddress(new LinkAddress("192.0.2.163/25")); + wifiLp.addRoute(new RouteInfo(new IpPrefix("0.0.0.0/0"), null, "wlan0")); + final NetworkCapabilities wifiNc = new NetworkCapabilities(); + wifiNc.addTransportType(TRANSPORT_WIFI); + wifiNc.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp, wifiNc); + + b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.DISCONNECTED); + // Wifi is CONNECTING because the VPN isn't up yet. + b2 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTING); + ExpectedBroadcast b3 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED); + mWiFiNetworkAgent.connect(false /* validated */); + b1.expectBroadcast(); + b2.expectBroadcast(); + b3.expectBroadcast(); + mMockVpn.expectStopVpnRunnerPrivileged(); + mMockVpn.expectStartLegacyVpnRunner(); + + // TODO: why is wifi not blocked? Is it because when this callback is sent, the VPN is still + // connected, so the network is not considered blocked by the lockdown UID ranges? But the + // fact that a VPN is connected should only result in the VPN itself being unblocked, not + // any other network. Bug in isUidBlockedByVpn? + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + callback.expectCallback(CallbackEntry.LOST, mMockVpn); + defaultCallback.expectCallback(CallbackEntry.LOST, mMockVpn); + defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mWiFiNetworkAgent); + systemDefaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + + // While the VPN is reconnecting on the new network, everything is blocked. + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_VPN, DetailedState.BLOCKED); + assertExtraInfoFromCmBlocked(mWiFiNetworkAgent); + + // The VPN comes up again on wifi. + b1 = expectConnectivityAction(TYPE_VPN, DetailedState.CONNECTED); + b2 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED); + establishLegacyLockdownVpn(mWiFiNetworkAgent.getNetwork()); + callback.expectAvailableThenValidatedCallbacks(mMockVpn); + defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn); + systemDefaultCallback.assertNoCallback(); + b1.expectBroadcast(); + b2.expectBroadcast(); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED); + assertExtraInfoFromCmPresent(mWiFiNetworkAgent); + vpnNc = mCm.getNetworkCapabilities(mMockVpn.getNetwork()); + assertTrue(vpnNc.hasTransport(TRANSPORT_VPN)); + assertTrue(vpnNc.hasTransport(TRANSPORT_WIFI)); + assertFalse(vpnNc.hasTransport(TRANSPORT_CELLULAR)); + assertTrue(vpnNc.hasCapability(NET_CAPABILITY_NOT_METERED)); + + // Disconnect cell. Nothing much happens since it's not the default network. + mCellNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + defaultCallback.assertNoCallback(); + systemDefaultCallback.assertNoCallback(); + + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED); + assertExtraInfoFromCmPresent(mWiFiNetworkAgent); + + b1 = expectConnectivityAction(TYPE_WIFI, DetailedState.DISCONNECTED); + b2 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED); + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + systemDefaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + b1.expectBroadcast(); + callback.expectCapabilitiesThat(mMockVpn, nc -> !nc.hasTransport(TRANSPORT_WIFI)); + mMockVpn.expectStopVpnRunnerPrivileged(); + callback.expectCallback(CallbackEntry.LOST, mMockVpn); + b2.expectBroadcast(); + } + + /** + * Test mutable and requestable network capabilities such as + * {@link NetworkCapabilities#NET_CAPABILITY_TRUSTED} and + * {@link NetworkCapabilities#NET_CAPABILITY_NOT_VCN_MANAGED}. Verify that the + * {@code ConnectivityService} re-assign the networks accordingly. + */ + @Test + public final void testLoseMutableAndRequestableCaps() throws Exception { + final int[] testCaps = new int [] { + NET_CAPABILITY_TRUSTED, + NET_CAPABILITY_NOT_VCN_MANAGED + }; + for (final int testCap : testCaps) { + // Create requests with and without the testing capability. + final TestNetworkCallback callbackWithCap = new TestNetworkCallback(); + final TestNetworkCallback callbackWithoutCap = new TestNetworkCallback(); + mCm.requestNetwork(new NetworkRequest.Builder().addCapability(testCap).build(), + callbackWithCap); + mCm.requestNetwork(new NetworkRequest.Builder().removeCapability(testCap).build(), + callbackWithoutCap); + + // Setup networks with testing capability and verify the default network changes. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.addCapability(testCap); + mCellNetworkAgent.connect(true); + callbackWithCap.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + callbackWithoutCap.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + verify(mMockNetd).networkSetDefault(eq(mCellNetworkAgent.getNetwork().netId)); + reset(mMockNetd); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(testCap); + mWiFiNetworkAgent.connect(true); + callbackWithCap.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + callbackWithoutCap.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + verify(mMockNetd).networkSetDefault(eq(mWiFiNetworkAgent.getNetwork().netId)); + reset(mMockNetd); + + // Remove the testing capability on wifi, verify the callback and default network + // changes back to cellular. + mWiFiNetworkAgent.removeCapability(testCap); + callbackWithCap.expectAvailableCallbacksValidated(mCellNetworkAgent); + callbackWithoutCap.expectCapabilitiesWithout(testCap, mWiFiNetworkAgent); + verify(mMockNetd).networkSetDefault(eq(mCellNetworkAgent.getNetwork().netId)); + reset(mMockNetd); + + mCellNetworkAgent.removeCapability(testCap); + callbackWithCap.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + callbackWithoutCap.assertNoCallback(); + verify(mMockNetd).networkClearDefault(); + + mCm.unregisterNetworkCallback(callbackWithCap); + mCm.unregisterNetworkCallback(callbackWithoutCap); + } + } + + @Test + public final void testBatteryStatsNetworkType() throws Exception { + final LinkProperties cellLp = new LinkProperties(); + cellLp.setInterfaceName("cell0"); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp); + mCellNetworkAgent.connect(true); + waitForIdle(); + verify(mDeps).reportNetworkInterfaceForTransports(mServiceContext, + cellLp.getInterfaceName(), + new int[] { TRANSPORT_CELLULAR }); + + final LinkProperties wifiLp = new LinkProperties(); + wifiLp.setInterfaceName("wifi0"); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp); + mWiFiNetworkAgent.connect(true); + waitForIdle(); + verify(mDeps).reportNetworkInterfaceForTransports(mServiceContext, + wifiLp.getInterfaceName(), + new int[] { TRANSPORT_WIFI }); + + mCellNetworkAgent.disconnect(); + mWiFiNetworkAgent.disconnect(); + + cellLp.setInterfaceName("wifi0"); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp); + mCellNetworkAgent.connect(true); + waitForIdle(); + verify(mDeps).reportNetworkInterfaceForTransports(mServiceContext, + cellLp.getInterfaceName(), + new int[] { TRANSPORT_CELLULAR }); + mCellNetworkAgent.disconnect(); + } + + /** + * Make simulated InterfaceConfigParcel for Nat464Xlat to query clat lower layer info. + */ + private InterfaceConfigurationParcel getClatInterfaceConfigParcel(LinkAddress la) { + final InterfaceConfigurationParcel cfg = new InterfaceConfigurationParcel(); + cfg.hwAddr = "11:22:33:44:55:66"; + cfg.ipv4Addr = la.getAddress().getHostAddress(); + cfg.prefixLength = la.getPrefixLength(); + return cfg; + } + + /** + * Make expected stack link properties, copied from Nat464Xlat. + */ + private LinkProperties makeClatLinkProperties(LinkAddress la) { + LinkAddress clatAddress = la; + LinkProperties stacked = new LinkProperties(); + stacked.setInterfaceName(CLAT_PREFIX + MOBILE_IFNAME); + RouteInfo ipv4Default = new RouteInfo( + new LinkAddress(Inet4Address.ANY, 0), + clatAddress.getAddress(), CLAT_PREFIX + MOBILE_IFNAME); + stacked.addRoute(ipv4Default); + stacked.addLinkAddress(clatAddress); + return stacked; + } + + private Nat64PrefixEventParcel makeNat64PrefixEvent(final int netId, final int prefixOperation, + final String prefixAddress, final int prefixLength) { + final Nat64PrefixEventParcel event = new Nat64PrefixEventParcel(); + event.netId = netId; + event.prefixOperation = prefixOperation; + event.prefixAddress = prefixAddress; + event.prefixLength = prefixLength; + return event; + } + + @Test + public void testStackedLinkProperties() throws Exception { + final LinkAddress myIpv4 = new LinkAddress("1.2.3.4/24"); + final LinkAddress myIpv6 = new LinkAddress("2001:db8:1::1/64"); + final String kNat64PrefixString = "2001:db8:64:64:64:64::"; + final IpPrefix kNat64Prefix = new IpPrefix(InetAddress.getByName(kNat64PrefixString), 96); + final String kOtherNat64PrefixString = "64:ff9b::"; + final IpPrefix kOtherNat64Prefix = new IpPrefix( + InetAddress.getByName(kOtherNat64PrefixString), 96); + final RouteInfo defaultRoute = new RouteInfo((IpPrefix) null, myIpv6.getAddress(), + MOBILE_IFNAME); + final RouteInfo ipv6Subnet = new RouteInfo(myIpv6, null, MOBILE_IFNAME); + final RouteInfo ipv4Subnet = new RouteInfo(myIpv4, null, MOBILE_IFNAME); + final RouteInfo stackedDefault = new RouteInfo((IpPrefix) null, myIpv4.getAddress(), + CLAT_PREFIX + MOBILE_IFNAME); + + final NetworkRequest networkRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_INTERNET) + .build(); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(networkRequest, networkCallback); + + // Prepare ipv6 only link properties. + final LinkProperties cellLp = new LinkProperties(); + cellLp.setInterfaceName(MOBILE_IFNAME); + cellLp.addLinkAddress(myIpv6); + cellLp.addRoute(defaultRoute); + cellLp.addRoute(ipv6Subnet); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp); + reset(mMockDnsResolver); + reset(mMockNetd); + + // Connect with ipv6 link properties. Expect prefix discovery to be started. + mCellNetworkAgent.connect(true); + final int cellNetId = mCellNetworkAgent.getNetwork().netId; + waitForIdle(); + + verify(mMockNetd, times(1)).networkCreate(nativeNetworkConfigPhysical(cellNetId, + INetd.PERMISSION_NONE)); + assertRoutesAdded(cellNetId, ipv6Subnet, defaultRoute); + verify(mMockDnsResolver, times(1)).createNetworkCache(eq(cellNetId)); + verify(mMockNetd, times(1)).networkAddInterface(cellNetId, MOBILE_IFNAME); + verify(mDeps).reportNetworkInterfaceForTransports(mServiceContext, + cellLp.getInterfaceName(), + new int[] { TRANSPORT_CELLULAR }); + + networkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + verify(mMockDnsResolver, times(1)).startPrefix64Discovery(cellNetId); + + // Switching default network updates TCP buffer sizes. + verifyTcpBufferSizeChange(ConnectivityService.DEFAULT_TCP_BUFFER_SIZES); + // Add an IPv4 address. Expect prefix discovery to be stopped. Netd doesn't tell us that + // the NAT64 prefix was removed because one was never discovered. + cellLp.addLinkAddress(myIpv4); + mCellNetworkAgent.sendLinkProperties(cellLp); + networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + assertRoutesAdded(cellNetId, ipv4Subnet); + verify(mMockDnsResolver, times(1)).stopPrefix64Discovery(cellNetId); + verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration(any()); + + // Make sure BatteryStats was not told about any v4- interfaces, as none should have + // come online yet. + waitForIdle(); + verify(mDeps, never()) + .reportNetworkInterfaceForTransports(eq(mServiceContext), startsWith("v4-"), any()); + + verifyNoMoreInteractions(mMockNetd); + verifyNoMoreInteractions(mMockDnsResolver); + reset(mMockNetd); + reset(mMockDnsResolver); + when(mMockNetd.interfaceGetCfg(CLAT_PREFIX + MOBILE_IFNAME)) + .thenReturn(getClatInterfaceConfigParcel(myIpv4)); + + // Remove IPv4 address. Expect prefix discovery to be started again. + cellLp.removeLinkAddress(myIpv4); + mCellNetworkAgent.sendLinkProperties(cellLp); + networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + verify(mMockDnsResolver, times(1)).startPrefix64Discovery(cellNetId); + assertRoutesRemoved(cellNetId, ipv4Subnet); + + // When NAT64 prefix discovery succeeds, LinkProperties are updated and clatd is started. + Nat464Xlat clat = getNat464Xlat(mCellNetworkAgent); + assertNull(mCm.getLinkProperties(mCellNetworkAgent.getNetwork()).getNat64Prefix()); + mService.mResolverUnsolEventCallback.onNat64PrefixEvent( + makeNat64PrefixEvent(cellNetId, PREFIX_OPERATION_ADDED, kNat64PrefixString, 96)); + LinkProperties lpBeforeClat = networkCallback.expectCallback( + CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent).getLp(); + assertEquals(0, lpBeforeClat.getStackedLinks().size()); + assertEquals(kNat64Prefix, lpBeforeClat.getNat64Prefix()); + verify(mMockNetd, times(1)).clatdStart(MOBILE_IFNAME, kNat64Prefix.toString()); + + // Clat iface comes up. Expect stacked link to be added. + clat.interfaceLinkStateChanged(CLAT_PREFIX + MOBILE_IFNAME, true); + networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + List stackedLps = mCm.getLinkProperties(mCellNetworkAgent.getNetwork()) + .getStackedLinks(); + assertEquals(makeClatLinkProperties(myIpv4), stackedLps.get(0)); + assertRoutesAdded(cellNetId, stackedDefault); + verify(mMockNetd, times(1)).networkAddInterface(cellNetId, CLAT_PREFIX + MOBILE_IFNAME); + // Change trivial linkproperties and see if stacked link is preserved. + cellLp.addDnsServer(InetAddress.getByName("8.8.8.8")); + mCellNetworkAgent.sendLinkProperties(cellLp); + networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + + List stackedLpsAfterChange = + mCm.getLinkProperties(mCellNetworkAgent.getNetwork()).getStackedLinks(); + assertNotEquals(stackedLpsAfterChange, Collections.EMPTY_LIST); + assertEquals(makeClatLinkProperties(myIpv4), stackedLpsAfterChange.get(0)); + + verify(mMockDnsResolver, times(1)).setResolverConfiguration( + mResolverParamsParcelCaptor.capture()); + ResolverParamsParcel resolvrParams = mResolverParamsParcelCaptor.getValue(); + assertEquals(1, resolvrParams.servers.length); + assertTrue(ArrayUtils.contains(resolvrParams.servers, "8.8.8.8")); + + for (final LinkProperties stackedLp : stackedLpsAfterChange) { + verify(mDeps).reportNetworkInterfaceForTransports( + mServiceContext, stackedLp.getInterfaceName(), + new int[] { TRANSPORT_CELLULAR }); + } + reset(mMockNetd); + when(mMockNetd.interfaceGetCfg(CLAT_PREFIX + MOBILE_IFNAME)) + .thenReturn(getClatInterfaceConfigParcel(myIpv4)); + // Change the NAT64 prefix without first removing it. + // Expect clatd to be stopped and started with the new prefix. + mService.mResolverUnsolEventCallback.onNat64PrefixEvent(makeNat64PrefixEvent( + cellNetId, PREFIX_OPERATION_ADDED, kOtherNat64PrefixString, 96)); + networkCallback.expectLinkPropertiesThat(mCellNetworkAgent, + (lp) -> lp.getStackedLinks().size() == 0); + verify(mMockNetd, times(1)).clatdStop(MOBILE_IFNAME); + assertRoutesRemoved(cellNetId, stackedDefault); + verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_PREFIX + MOBILE_IFNAME); + + verify(mMockNetd, times(1)).clatdStart(MOBILE_IFNAME, kOtherNat64Prefix.toString()); + networkCallback.expectLinkPropertiesThat(mCellNetworkAgent, + (lp) -> lp.getNat64Prefix().equals(kOtherNat64Prefix)); + clat.interfaceLinkStateChanged(CLAT_PREFIX + MOBILE_IFNAME, true); + networkCallback.expectLinkPropertiesThat(mCellNetworkAgent, + (lp) -> lp.getStackedLinks().size() == 1); + assertRoutesAdded(cellNetId, stackedDefault); + verify(mMockNetd, times(1)).networkAddInterface(cellNetId, CLAT_PREFIX + MOBILE_IFNAME); + reset(mMockNetd); + + // Add ipv4 address, expect that clatd and prefix discovery are stopped and stacked + // linkproperties are cleaned up. + cellLp.addLinkAddress(myIpv4); + cellLp.addRoute(ipv4Subnet); + mCellNetworkAgent.sendLinkProperties(cellLp); + networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + assertRoutesAdded(cellNetId, ipv4Subnet); + verify(mMockNetd, times(1)).clatdStop(MOBILE_IFNAME); + verify(mMockDnsResolver, times(1)).stopPrefix64Discovery(cellNetId); + + // As soon as stop is called, the linkproperties lose the stacked interface. + networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + LinkProperties actualLpAfterIpv4 = mCm.getLinkProperties(mCellNetworkAgent.getNetwork()); + LinkProperties expected = new LinkProperties(cellLp); + expected.setNat64Prefix(kOtherNat64Prefix); + assertEquals(expected, actualLpAfterIpv4); + assertEquals(0, actualLpAfterIpv4.getStackedLinks().size()); + assertRoutesRemoved(cellNetId, stackedDefault); + + // The interface removed callback happens but has no effect after stop is called. + clat.interfaceRemoved(CLAT_PREFIX + MOBILE_IFNAME); + networkCallback.assertNoCallback(); + verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_PREFIX + MOBILE_IFNAME); + verifyNoMoreInteractions(mMockNetd); + verifyNoMoreInteractions(mMockDnsResolver); + reset(mMockNetd); + reset(mMockDnsResolver); + when(mMockNetd.interfaceGetCfg(CLAT_PREFIX + MOBILE_IFNAME)) + .thenReturn(getClatInterfaceConfigParcel(myIpv4)); + + // Stopping prefix discovery causes netd to tell us that the NAT64 prefix is gone. + mService.mResolverUnsolEventCallback.onNat64PrefixEvent(makeNat64PrefixEvent( + cellNetId, PREFIX_OPERATION_REMOVED, kOtherNat64PrefixString, 96)); + networkCallback.expectLinkPropertiesThat(mCellNetworkAgent, + (lp) -> lp.getNat64Prefix() == null); + + // Remove IPv4 address and expect prefix discovery and clatd to be started again. + cellLp.removeLinkAddress(myIpv4); + cellLp.removeRoute(new RouteInfo(myIpv4, null, MOBILE_IFNAME)); + cellLp.removeDnsServer(InetAddress.getByName("8.8.8.8")); + mCellNetworkAgent.sendLinkProperties(cellLp); + networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + assertRoutesRemoved(cellNetId, ipv4Subnet); // Directly-connected routes auto-added. + verify(mMockDnsResolver, times(1)).startPrefix64Discovery(cellNetId); + mService.mResolverUnsolEventCallback.onNat64PrefixEvent(makeNat64PrefixEvent( + cellNetId, PREFIX_OPERATION_ADDED, kNat64PrefixString, 96)); + networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + verify(mMockNetd, times(1)).clatdStart(MOBILE_IFNAME, kNat64Prefix.toString()); + + // Clat iface comes up. Expect stacked link to be added. + clat.interfaceLinkStateChanged(CLAT_PREFIX + MOBILE_IFNAME, true); + networkCallback.expectLinkPropertiesThat(mCellNetworkAgent, + (lp) -> lp.getStackedLinks().size() == 1 && lp.getNat64Prefix() != null); + assertRoutesAdded(cellNetId, stackedDefault); + verify(mMockNetd, times(1)).networkAddInterface(cellNetId, CLAT_PREFIX + MOBILE_IFNAME); + + // NAT64 prefix is removed. Expect that clat is stopped. + mService.mResolverUnsolEventCallback.onNat64PrefixEvent(makeNat64PrefixEvent( + cellNetId, PREFIX_OPERATION_REMOVED, kNat64PrefixString, 96)); + networkCallback.expectLinkPropertiesThat(mCellNetworkAgent, + (lp) -> lp.getStackedLinks().size() == 0 && lp.getNat64Prefix() == null); + assertRoutesRemoved(cellNetId, ipv4Subnet, stackedDefault); + + // Stop has no effect because clat is already stopped. + verify(mMockNetd, times(1)).clatdStop(MOBILE_IFNAME); + networkCallback.expectLinkPropertiesThat(mCellNetworkAgent, + (lp) -> lp.getStackedLinks().size() == 0); + verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_PREFIX + MOBILE_IFNAME); + verify(mMockNetd, times(1)).interfaceGetCfg(CLAT_PREFIX + MOBILE_IFNAME); + verifyNoMoreInteractions(mMockNetd); + // Clean up. + mCellNetworkAgent.disconnect(); + networkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + networkCallback.assertNoCallback(); + mCm.unregisterNetworkCallback(networkCallback); + } + + private void expectNat64PrefixChange(TestableNetworkCallback callback, + TestNetworkAgentWrapper agent, IpPrefix prefix) { + callback.expectLinkPropertiesThat(agent, x -> Objects.equals(x.getNat64Prefix(), prefix)); + } + + @Test + public void testNat64PrefixMultipleSources() throws Exception { + final String iface = "wlan0"; + final String pref64FromRaStr = "64:ff9b::"; + final String pref64FromDnsStr = "2001:db8:64::"; + final IpPrefix pref64FromRa = new IpPrefix(InetAddress.getByName(pref64FromRaStr), 96); + final IpPrefix pref64FromDns = new IpPrefix(InetAddress.getByName(pref64FromDnsStr), 96); + final IpPrefix newPref64FromRa = new IpPrefix("2001:db8:64:64:64:64::/96"); + + final NetworkRequest request = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_INTERNET) + .build(); + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + final LinkProperties baseLp = new LinkProperties(); + baseLp.setInterfaceName(iface); + baseLp.addLinkAddress(new LinkAddress("2001:db8:1::1/64")); + baseLp.addDnsServer(InetAddress.getByName("2001:4860:4860::6464")); + + reset(mMockNetd, mMockDnsResolver); + InOrder inOrder = inOrder(mMockNetd, mMockDnsResolver); + + // If a network already has a NAT64 prefix on connect, clatd is started immediately and + // prefix discovery is never started. + LinkProperties lp = new LinkProperties(baseLp); + lp.setNat64Prefix(pref64FromRa); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, lp); + mWiFiNetworkAgent.connect(false); + final Network network = mWiFiNetworkAgent.getNetwork(); + int netId = network.getNetId(); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString()); + inOrder.verify(mMockDnsResolver).setPrefix64(netId, pref64FromRa.toString()); + inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId); + callback.assertNoCallback(); + assertEquals(pref64FromRa, mCm.getLinkProperties(network).getNat64Prefix()); + + // If the RA prefix is withdrawn, clatd is stopped and prefix discovery is started. + lp.setNat64Prefix(null); + mWiFiNetworkAgent.sendLinkProperties(lp); + expectNat64PrefixChange(callback, mWiFiNetworkAgent, null); + inOrder.verify(mMockNetd).clatdStop(iface); + inOrder.verify(mMockDnsResolver).setPrefix64(netId, ""); + inOrder.verify(mMockDnsResolver).startPrefix64Discovery(netId); + + // If the RA prefix appears while DNS discovery is in progress, discovery is stopped and + // clatd is started with the prefix from the RA. + lp.setNat64Prefix(pref64FromRa); + mWiFiNetworkAgent.sendLinkProperties(lp); + expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromRa); + inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString()); + inOrder.verify(mMockDnsResolver).stopPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver).setPrefix64(netId, pref64FromRa.toString()); + + // Withdraw the RA prefix so we can test the case where an RA prefix appears after DNS + // discovery has succeeded. + lp.setNat64Prefix(null); + mWiFiNetworkAgent.sendLinkProperties(lp); + expectNat64PrefixChange(callback, mWiFiNetworkAgent, null); + inOrder.verify(mMockNetd).clatdStop(iface); + inOrder.verify(mMockDnsResolver).setPrefix64(netId, ""); + inOrder.verify(mMockDnsResolver).startPrefix64Discovery(netId); + + mService.mResolverUnsolEventCallback.onNat64PrefixEvent( + makeNat64PrefixEvent(netId, PREFIX_OPERATION_ADDED, pref64FromDnsStr, 96)); + expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromDns); + inOrder.verify(mMockNetd).clatdStart(iface, pref64FromDns.toString()); + + // If an RA advertises the same prefix that was discovered by DNS, nothing happens: prefix + // discovery is not stopped, and there are no callbacks. + lp.setNat64Prefix(pref64FromDns); + mWiFiNetworkAgent.sendLinkProperties(lp); + callback.assertNoCallback(); + inOrder.verify(mMockNetd, never()).clatdStop(iface); + inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString()); + inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString()); + + // If the RA is later withdrawn, nothing happens again. + lp.setNat64Prefix(null); + mWiFiNetworkAgent.sendLinkProperties(lp); + callback.assertNoCallback(); + inOrder.verify(mMockNetd, never()).clatdStop(iface); + inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString()); + inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString()); + + // If the RA prefix changes, clatd is restarted and prefix discovery is stopped. + lp.setNat64Prefix(pref64FromRa); + mWiFiNetworkAgent.sendLinkProperties(lp); + expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromRa); + inOrder.verify(mMockNetd).clatdStop(iface); + inOrder.verify(mMockDnsResolver).stopPrefix64Discovery(netId); + + // Stopping prefix discovery results in a prefix removed notification. + mService.mResolverUnsolEventCallback.onNat64PrefixEvent( + makeNat64PrefixEvent(netId, PREFIX_OPERATION_REMOVED, pref64FromDnsStr, 96)); + + inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString()); + inOrder.verify(mMockDnsResolver).setPrefix64(netId, pref64FromRa.toString()); + inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId); + + // If the RA prefix changes, clatd is restarted and prefix discovery is not started. + lp.setNat64Prefix(newPref64FromRa); + mWiFiNetworkAgent.sendLinkProperties(lp); + expectNat64PrefixChange(callback, mWiFiNetworkAgent, newPref64FromRa); + inOrder.verify(mMockNetd).clatdStop(iface); + inOrder.verify(mMockDnsResolver).setPrefix64(netId, ""); + inOrder.verify(mMockNetd).clatdStart(iface, newPref64FromRa.toString()); + inOrder.verify(mMockDnsResolver).setPrefix64(netId, newPref64FromRa.toString()); + inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId); + + // If the RA prefix changes to the same value, nothing happens. + lp.setNat64Prefix(newPref64FromRa); + mWiFiNetworkAgent.sendLinkProperties(lp); + callback.assertNoCallback(); + assertEquals(newPref64FromRa, mCm.getLinkProperties(network).getNat64Prefix()); + inOrder.verify(mMockNetd, never()).clatdStop(iface); + inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString()); + inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString()); + + // The transition between no prefix and DNS prefix is tested in testStackedLinkProperties. + + // If the same prefix is learned first by DNS and then by RA, and clat is later stopped, + // (e.g., because the network disconnects) setPrefix64(netid, "") is never called. + lp.setNat64Prefix(null); + mWiFiNetworkAgent.sendLinkProperties(lp); + expectNat64PrefixChange(callback, mWiFiNetworkAgent, null); + inOrder.verify(mMockNetd).clatdStop(iface); + inOrder.verify(mMockDnsResolver).setPrefix64(netId, ""); + inOrder.verify(mMockDnsResolver).startPrefix64Discovery(netId); + mService.mResolverUnsolEventCallback.onNat64PrefixEvent( + makeNat64PrefixEvent(netId, PREFIX_OPERATION_ADDED, pref64FromDnsStr, 96)); + expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromDns); + inOrder.verify(mMockNetd).clatdStart(iface, pref64FromDns.toString()); + inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), any()); + + lp.setNat64Prefix(pref64FromDns); + mWiFiNetworkAgent.sendLinkProperties(lp); + callback.assertNoCallback(); + inOrder.verify(mMockNetd, never()).clatdStop(iface); + inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString()); + inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString()); + + // When tearing down a network, clat state is only updated after CALLBACK_LOST is fired, but + // before CONNECTIVITY_ACTION is sent. Wait for CONNECTIVITY_ACTION before verifying that + // clat has been stopped, or the test will be flaky. + ExpectedBroadcast b = expectConnectivityAction(TYPE_WIFI, DetailedState.DISCONNECTED); + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + b.expectBroadcast(); + + inOrder.verify(mMockNetd).clatdStop(iface); + inOrder.verify(mMockDnsResolver).stopPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString()); + + mCm.unregisterNetworkCallback(callback); + } + + @Test + public void testWith464XlatDisable() throws Exception { + doReturn(false).when(mDeps).getCellular464XlatEnabled(); + + final TestNetworkCallback callback = new TestNetworkCallback(); + final TestNetworkCallback defaultCallback = new TestNetworkCallback(); + final NetworkRequest networkRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_INTERNET) + .build(); + mCm.registerNetworkCallback(networkRequest, callback); + mCm.registerDefaultNetworkCallback(defaultCallback); + + // Bring up validated cell. + final LinkProperties cellLp = new LinkProperties(); + cellLp.setInterfaceName(MOBILE_IFNAME); + cellLp.addLinkAddress(new LinkAddress("2001:db8:1::1/64")); + cellLp.addRoute(new RouteInfo(new IpPrefix("::/0"), null, MOBILE_IFNAME)); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + + mCellNetworkAgent.sendLinkProperties(cellLp); + mCellNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + defaultCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + final int cellNetId = mCellNetworkAgent.getNetwork().netId; + waitForIdle(); + + verify(mMockDnsResolver, never()).startPrefix64Discovery(cellNetId); + Nat464Xlat clat = getNat464Xlat(mCellNetworkAgent); + assertTrue("Nat464Xlat was not IDLE", !clat.isStarted()); + + // This cannot happen because prefix discovery cannot succeed if it is never started. + mService.mResolverUnsolEventCallback.onNat64PrefixEvent( + makeNat64PrefixEvent(cellNetId, PREFIX_OPERATION_ADDED, "64:ff9b::", 96)); + + // ... but still, check that even if it did, clatd would not be started. + verify(mMockNetd, never()).clatdStart(anyString(), anyString()); + assertTrue("Nat464Xlat was not IDLE", !clat.isStarted()); + } + + @Test + public void testDataActivityTracking() throws Exception { + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + final NetworkRequest networkRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_INTERNET) + .build(); + mCm.registerNetworkCallback(networkRequest, networkCallback); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + final LinkProperties cellLp = new LinkProperties(); + cellLp.setInterfaceName(MOBILE_IFNAME); + mCellNetworkAgent.sendLinkProperties(cellLp); + mCellNetworkAgent.connect(true); + networkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + verify(mMockNetd, times(1)).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(), + eq(Integer.toString(TRANSPORT_CELLULAR))); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + final LinkProperties wifiLp = new LinkProperties(); + wifiLp.setInterfaceName(WIFI_IFNAME); + mWiFiNetworkAgent.sendLinkProperties(wifiLp); + + // Network switch + mWiFiNetworkAgent.connect(true); + networkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + networkCallback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + networkCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + verify(mMockNetd, times(1)).idletimerAddInterface(eq(WIFI_IFNAME), anyInt(), + eq(Integer.toString(TRANSPORT_WIFI))); + verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(), + eq(Integer.toString(TRANSPORT_CELLULAR))); + + // Disconnect wifi and switch back to cell + reset(mMockNetd); + mWiFiNetworkAgent.disconnect(); + networkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + assertNoCallbacks(networkCallback); + verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(WIFI_IFNAME), anyInt(), + eq(Integer.toString(TRANSPORT_WIFI))); + verify(mMockNetd, times(1)).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(), + eq(Integer.toString(TRANSPORT_CELLULAR))); + + // reconnect wifi + reset(mMockNetd); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + wifiLp.setInterfaceName(WIFI_IFNAME); + mWiFiNetworkAgent.sendLinkProperties(wifiLp); + mWiFiNetworkAgent.connect(true); + networkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + networkCallback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + networkCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + verify(mMockNetd, times(1)).idletimerAddInterface(eq(WIFI_IFNAME), anyInt(), + eq(Integer.toString(TRANSPORT_WIFI))); + verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(), + eq(Integer.toString(TRANSPORT_CELLULAR))); + + // Disconnect cell + reset(mMockNetd); + mCellNetworkAgent.disconnect(); + networkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + // LOST callback is triggered earlier than removing idle timer. Broadcast should also be + // sent as network being switched. Ensure rule removal for cell will not be triggered + // unexpectedly before network being removed. + waitForIdle(); + verify(mMockNetd, times(0)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(), + eq(Integer.toString(TRANSPORT_CELLULAR))); + verify(mMockNetd, times(1)).networkDestroy(eq(mCellNetworkAgent.getNetwork().netId)); + verify(mMockDnsResolver, times(1)) + .destroyNetworkCache(eq(mCellNetworkAgent.getNetwork().netId)); + + // Disconnect wifi + ExpectedBroadcast b = expectConnectivityAction(TYPE_WIFI, DetailedState.DISCONNECTED); + mWiFiNetworkAgent.disconnect(); + b.expectBroadcast(); + verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(WIFI_IFNAME), anyInt(), + eq(Integer.toString(TRANSPORT_WIFI))); + + // Clean up + mCm.unregisterNetworkCallback(networkCallback); + } + + private void verifyTcpBufferSizeChange(String tcpBufferSizes) throws Exception { + String[] values = tcpBufferSizes.split(","); + String rmemValues = String.join(" ", values[0], values[1], values[2]); + String wmemValues = String.join(" ", values[3], values[4], values[5]); + verify(mMockNetd, atLeastOnce()).setTcpRWmemorySize(rmemValues, wmemValues); + reset(mMockNetd); + } + + @Test + public void testTcpBufferReset() throws Exception { + final String testTcpBufferSizes = "1,2,3,4,5,6"; + final NetworkRequest networkRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_INTERNET) + .build(); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(networkRequest, networkCallback); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + reset(mMockNetd); + // Switching default network updates TCP buffer sizes. + mCellNetworkAgent.connect(false); + networkCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + verifyTcpBufferSizeChange(ConnectivityService.DEFAULT_TCP_BUFFER_SIZES); + // Change link Properties should have updated tcp buffer size. + LinkProperties lp = new LinkProperties(); + lp.setTcpBufferSizes(testTcpBufferSizes); + mCellNetworkAgent.sendLinkProperties(lp); + networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + verifyTcpBufferSizeChange(testTcpBufferSizes); + // Clean up. + mCellNetworkAgent.disconnect(); + networkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + networkCallback.assertNoCallback(); + mCm.unregisterNetworkCallback(networkCallback); + } + + @Test + public void testGetGlobalProxyForNetwork() throws Exception { + final ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("test", 8888); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + final Network wifiNetwork = mWiFiNetworkAgent.getNetwork(); + when(mService.mProxyTracker.getGlobalProxy()).thenReturn(testProxyInfo); + assertEquals(testProxyInfo, mService.getProxyForNetwork(wifiNetwork)); + } + + @Test + public void testGetProxyForActiveNetwork() throws Exception { + final ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("test", 8888); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + waitForIdle(); + assertNull(mService.getProxyForNetwork(null)); + + final LinkProperties testLinkProperties = new LinkProperties(); + testLinkProperties.setHttpProxy(testProxyInfo); + + mWiFiNetworkAgent.sendLinkProperties(testLinkProperties); + waitForIdle(); + + assertEquals(testProxyInfo, mService.getProxyForNetwork(null)); + } + + @Test + public void testGetProxyForVPN() throws Exception { + final ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("test", 8888); + + // Set up a WiFi network with no proxy + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + waitForIdle(); + assertNull(mService.getProxyForNetwork(null)); + + // Connect a VPN network with a proxy. + LinkProperties testLinkProperties = new LinkProperties(); + testLinkProperties.setHttpProxy(testProxyInfo); + mMockVpn.establishForMyUid(testLinkProperties); + assertUidRangesUpdatedForMyUid(true); + + // Test that the VPN network returns a proxy, and the WiFi does not. + assertEquals(testProxyInfo, mService.getProxyForNetwork(mMockVpn.getNetwork())); + assertEquals(testProxyInfo, mService.getProxyForNetwork(null)); + assertNull(mService.getProxyForNetwork(mWiFiNetworkAgent.getNetwork())); + + // Test that the VPN network returns no proxy when it is set to null. + testLinkProperties.setHttpProxy(null); + mMockVpn.sendLinkProperties(testLinkProperties); + waitForIdle(); + assertNull(mService.getProxyForNetwork(mMockVpn.getNetwork())); + assertNull(mService.getProxyForNetwork(null)); + + // Set WiFi proxy and check that the vpn proxy is still null. + testLinkProperties.setHttpProxy(testProxyInfo); + mWiFiNetworkAgent.sendLinkProperties(testLinkProperties); + waitForIdle(); + assertNull(mService.getProxyForNetwork(null)); + + // Disconnect from VPN and check that the active network, which is now the WiFi, has the + // correct proxy setting. + mMockVpn.disconnect(); + waitForIdle(); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertEquals(testProxyInfo, mService.getProxyForNetwork(mWiFiNetworkAgent.getNetwork())); + assertEquals(testProxyInfo, mService.getProxyForNetwork(null)); + } + + @Test + public void testFullyRoutedVpnResultsInInterfaceFilteringRules() throws Exception { + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("tun0"); + lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null)); + lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE)); + // The uid range needs to cover the test app so the network is visible to it. + final Set vpnRange = Collections.singleton(PRIMARY_UIDRANGE); + mMockVpn.establish(lp, VPN_UID, vpnRange); + assertVpnUidRangesUpdated(true, vpnRange, VPN_UID); + + // A connected VPN should have interface rules set up. There are two expected invocations, + // one during the VPN initial connection, one during the VPN LinkProperties update. + ArgumentCaptor uidCaptor = ArgumentCaptor.forClass(int[].class); + verify(mMockNetd, times(2)).firewallAddUidInterfaceRules(eq("tun0"), uidCaptor.capture()); + assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID); + assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID); + assertTrue(mService.mPermissionMonitor.getVpnUidRanges("tun0").equals(vpnRange)); + + mMockVpn.disconnect(); + waitForIdle(); + + // Disconnected VPN should have interface rules removed + verify(mMockNetd).firewallRemoveUidInterfaceRules(uidCaptor.capture()); + assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID); + assertNull(mService.mPermissionMonitor.getVpnUidRanges("tun0")); + } + + @Test + public void testLegacyVpnDoesNotResultInInterfaceFilteringRule() throws Exception { + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("tun0"); + lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null)); + lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null)); + // The uid range needs to cover the test app so the network is visible to it. + final Set vpnRange = Collections.singleton(PRIMARY_UIDRANGE); + mMockVpn.establish(lp, Process.SYSTEM_UID, vpnRange); + assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID); + + // Legacy VPN should not have interface rules set up + verify(mMockNetd, never()).firewallAddUidInterfaceRules(any(), any()); + } + + @Test + public void testLocalIpv4OnlyVpnDoesNotResultInInterfaceFilteringRule() + throws Exception { + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("tun0"); + lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24"), null, "tun0")); + lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE)); + // The uid range needs to cover the test app so the network is visible to it. + final Set vpnRange = Collections.singleton(PRIMARY_UIDRANGE); + mMockVpn.establish(lp, Process.SYSTEM_UID, vpnRange); + assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID); + + // IPv6 unreachable route should not be misinterpreted as a default route + verify(mMockNetd, never()).firewallAddUidInterfaceRules(any(), any()); + } + + @Test + public void testVpnHandoverChangesInterfaceFilteringRule() throws Exception { + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("tun0"); + lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null)); + lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null)); + // The uid range needs to cover the test app so the network is visible to it. + final Set vpnRange = Collections.singleton(PRIMARY_UIDRANGE); + mMockVpn.establish(lp, VPN_UID, vpnRange); + assertVpnUidRangesUpdated(true, vpnRange, VPN_UID); + + // Connected VPN should have interface rules set up. There are two expected invocations, + // one during VPN uid update, one during VPN LinkProperties update + ArgumentCaptor uidCaptor = ArgumentCaptor.forClass(int[].class); + verify(mMockNetd, times(2)).firewallAddUidInterfaceRules(eq("tun0"), uidCaptor.capture()); + assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID); + assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID); + + reset(mMockNetd); + InOrder inOrder = inOrder(mMockNetd); + lp.setInterfaceName("tun1"); + mMockVpn.sendLinkProperties(lp); + waitForIdle(); + // VPN handover (switch to a new interface) should result in rules being updated (old rules + // removed first, then new rules added) + inOrder.verify(mMockNetd).firewallRemoveUidInterfaceRules(uidCaptor.capture()); + assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID); + inOrder.verify(mMockNetd).firewallAddUidInterfaceRules(eq("tun1"), uidCaptor.capture()); + assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID); + + reset(mMockNetd); + lp = new LinkProperties(); + lp.setInterfaceName("tun1"); + lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24"), null, "tun1")); + mMockVpn.sendLinkProperties(lp); + waitForIdle(); + // VPN not routing everything should no longer have interface filtering rules + verify(mMockNetd).firewallRemoveUidInterfaceRules(uidCaptor.capture()); + assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID); + + reset(mMockNetd); + lp = new LinkProperties(); + lp.setInterfaceName("tun1"); + lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), RTN_UNREACHABLE)); + lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null)); + mMockVpn.sendLinkProperties(lp); + waitForIdle(); + // Back to routing all IPv6 traffic should have filtering rules + verify(mMockNetd).firewallAddUidInterfaceRules(eq("tun1"), uidCaptor.capture()); + assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID); + } + + @Test + public void testUidUpdateChangesInterfaceFilteringRule() throws Exception { + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("tun0"); + lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), RTN_UNREACHABLE)); + lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null)); + // The uid range needs to cover the test app so the network is visible to it. + final UidRange vpnRange = PRIMARY_UIDRANGE; + final Set vpnRanges = Collections.singleton(vpnRange); + mMockVpn.establish(lp, VPN_UID, vpnRanges); + assertVpnUidRangesUpdated(true, vpnRanges, VPN_UID); + + reset(mMockNetd); + InOrder inOrder = inOrder(mMockNetd); + + // Update to new range which is old range minus APP1, i.e. only APP2 + final Set newRanges = new HashSet<>(Arrays.asList( + new UidRange(vpnRange.start, APP1_UID - 1), + new UidRange(APP1_UID + 1, vpnRange.stop))); + mMockVpn.setUids(newRanges); + waitForIdle(); + + ArgumentCaptor uidCaptor = ArgumentCaptor.forClass(int[].class); + // Verify old rules are removed before new rules are added + inOrder.verify(mMockNetd).firewallRemoveUidInterfaceRules(uidCaptor.capture()); + assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID); + inOrder.verify(mMockNetd).firewallAddUidInterfaceRules(eq("tun0"), uidCaptor.capture()); + assertContainsExactly(uidCaptor.getValue(), APP2_UID); + } + + @Test + public void testLinkPropertiesWithWakeOnLanForActiveNetwork() throws Exception { + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + + LinkProperties wifiLp = new LinkProperties(); + wifiLp.setInterfaceName(WIFI_WOL_IFNAME); + wifiLp.setWakeOnLanSupported(false); + + // Default network switch should update ifaces. + mWiFiNetworkAgent.connect(false); + mWiFiNetworkAgent.sendLinkProperties(wifiLp); + waitForIdle(); + + // ConnectivityService should have changed the WakeOnLanSupported to true + wifiLp.setWakeOnLanSupported(true); + assertEquals(wifiLp, mService.getActiveLinkProperties()); + } + + @Test + public void testLegacyExtraInfoSentToNetworkMonitor() throws Exception { + class TestNetworkAgent extends NetworkAgent { + TestNetworkAgent(Context context, Looper looper, NetworkAgentConfig config) { + super(context, looper, "MockAgent", new NetworkCapabilities(), + new LinkProperties(), 40 , config, null /* provider */); + } + } + final NetworkAgent naNoExtraInfo = new TestNetworkAgent( + mServiceContext, mCsHandlerThread.getLooper(), new NetworkAgentConfig()); + naNoExtraInfo.register(); + verify(mNetworkStack).makeNetworkMonitor(any(), isNull(String.class), any()); + naNoExtraInfo.unregister(); + + reset(mNetworkStack); + final NetworkAgentConfig config = + new NetworkAgentConfig.Builder().setLegacyExtraInfo("legacyinfo").build(); + final NetworkAgent naExtraInfo = new TestNetworkAgent( + mServiceContext, mCsHandlerThread.getLooper(), config); + naExtraInfo.register(); + verify(mNetworkStack).makeNetworkMonitor(any(), eq("legacyinfo"), any()); + naExtraInfo.unregister(); + } + + // To avoid granting location permission bypass. + private void denyAllLocationPrivilegedPermissions() { + mServiceContext.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + PERMISSION_DENIED); + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_DENIED); + mServiceContext.setPermission(Manifest.permission.NETWORK_STACK, + PERMISSION_DENIED); + mServiceContext.setPermission(Manifest.permission.NETWORK_SETUP_WIZARD, + PERMISSION_DENIED); + } + + private void setupLocationPermissions( + int targetSdk, boolean locationToggle, String op, String perm) throws Exception { + denyAllLocationPrivilegedPermissions(); + + final ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.targetSdkVersion = targetSdk; + when(mPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), any())) + .thenReturn(applicationInfo); + when(mPackageManager.getTargetSdkVersion(any())).thenReturn(targetSdk); + + when(mLocationManager.isLocationEnabledForUser(any())).thenReturn(locationToggle); + + if (op != null) { + when(mAppOpsManager.noteOp(eq(op), eq(Process.myUid()), + eq(mContext.getPackageName()), eq(getAttributionTag()), anyString())) + .thenReturn(AppOpsManager.MODE_ALLOWED); + } + + if (perm != null) { + mServiceContext.setPermission(perm, PERMISSION_GRANTED); + } + } + + private int getOwnerUidNetCapsPermission(int ownerUid, int callerUid, + boolean includeLocationSensitiveInfo) { + final NetworkCapabilities netCap = new NetworkCapabilities().setOwnerUid(ownerUid); + + return mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled( + netCap, includeLocationSensitiveInfo, Process.myUid(), callerUid, + mContext.getPackageName(), getAttributionTag()) + .getOwnerUid(); + } + + private void verifyTransportInfoCopyNetCapsPermission( + int callerUid, boolean includeLocationSensitiveInfo, + boolean shouldMakeCopyWithLocationSensitiveFieldsParcelable) { + final TransportInfo transportInfo = mock(TransportInfo.class); + when(transportInfo.getApplicableRedactions()).thenReturn(REDACT_FOR_ACCESS_FINE_LOCATION); + final NetworkCapabilities netCap = + new NetworkCapabilities().setTransportInfo(transportInfo); + + mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled( + netCap, includeLocationSensitiveInfo, Process.myPid(), callerUid, + mContext.getPackageName(), getAttributionTag()); + if (shouldMakeCopyWithLocationSensitiveFieldsParcelable) { + verify(transportInfo).makeCopy(REDACT_NONE); + } else { + verify(transportInfo).makeCopy(REDACT_FOR_ACCESS_FINE_LOCATION); + } + } + + private void verifyOwnerUidAndTransportInfoNetCapsPermission( + boolean shouldInclLocationSensitiveOwnerUidWithoutIncludeFlag, + boolean shouldInclLocationSensitiveOwnerUidWithIncludeFlag, + boolean shouldInclLocationSensitiveTransportInfoWithoutIncludeFlag, + boolean shouldInclLocationSensitiveTransportInfoWithIncludeFlag) { + final int myUid = Process.myUid(); + + final int expectedOwnerUidWithoutIncludeFlag = + shouldInclLocationSensitiveOwnerUidWithoutIncludeFlag + ? myUid : INVALID_UID; + assertEquals(expectedOwnerUidWithoutIncludeFlag, getOwnerUidNetCapsPermission( + myUid, myUid, false /* includeLocationSensitiveInfo */)); + + final int expectedOwnerUidWithIncludeFlag = + shouldInclLocationSensitiveOwnerUidWithIncludeFlag ? myUid : INVALID_UID; + assertEquals(expectedOwnerUidWithIncludeFlag, getOwnerUidNetCapsPermission( + myUid, myUid, true /* includeLocationSensitiveInfo */)); + + verifyTransportInfoCopyNetCapsPermission(myUid, + false, /* includeLocationSensitiveInfo */ + shouldInclLocationSensitiveTransportInfoWithoutIncludeFlag); + + verifyTransportInfoCopyNetCapsPermission(myUid, + true, /* includeLocationSensitiveInfo */ + shouldInclLocationSensitiveTransportInfoWithIncludeFlag); + + } + + private void verifyOwnerUidAndTransportInfoNetCapsPermissionPreS() { + verifyOwnerUidAndTransportInfoNetCapsPermission( + // Ensure that owner uid is included even if the request asks to remove it (which is + // the default) since the app has necessary permissions and targetSdk < S. + true, /* shouldInclLocationSensitiveOwnerUidWithoutIncludeFlag */ + true, /* shouldInclLocationSensitiveOwnerUidWithIncludeFlag */ + // Ensure that location info is removed if the request asks to remove it even if the + // app has necessary permissions. + false, /* shouldInclLocationSensitiveTransportInfoWithoutIncludeFlag */ + true /* shouldInclLocationSensitiveTransportInfoWithIncludeFlag */ + ); + } + + @Test + public void testCreateWithLocationInfoSanitizedWithFineLocationAfterQPreS() + throws Exception { + setupLocationPermissions(Build.VERSION_CODES.Q, true, AppOpsManager.OPSTR_FINE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION); + + verifyOwnerUidAndTransportInfoNetCapsPermissionPreS(); + } + + @Test + public void testCreateWithLocationInfoSanitizedWithFineLocationPreSWithAndWithoutCallbackFlag() + throws Exception { + setupLocationPermissions(Build.VERSION_CODES.R, true, AppOpsManager.OPSTR_FINE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION); + + verifyOwnerUidAndTransportInfoNetCapsPermissionPreS(); + } + + @Test + public void + testCreateWithLocationInfoSanitizedWithFineLocationAfterSWithAndWithoutCallbackFlag() + throws Exception { + setupLocationPermissions(Build.VERSION_CODES.S, true, AppOpsManager.OPSTR_FINE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION); + + verifyOwnerUidAndTransportInfoNetCapsPermission( + // Ensure that the owner UID is removed if the request asks us to remove it even + // if the app has necessary permissions since targetSdk >= S. + false, /* shouldInclLocationSensitiveOwnerUidWithoutIncludeFlag */ + true, /* shouldInclLocationSensitiveOwnerUidWithIncludeFlag */ + // Ensure that location info is removed if the request asks to remove it even if the + // app has necessary permissions. + false, /* shouldInclLocationSensitiveTransportInfoWithoutIncludeFlag */ + true /* shouldInclLocationSensitiveTransportInfoWithIncludeFlag */ + ); + } + + @Test + public void testCreateWithLocationInfoSanitizedWithCoarseLocationPreQ() + throws Exception { + setupLocationPermissions(Build.VERSION_CODES.P, true, AppOpsManager.OPSTR_COARSE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION); + + verifyOwnerUidAndTransportInfoNetCapsPermissionPreS(); + } + + private void verifyOwnerUidAndTransportInfoNetCapsNotIncluded() { + verifyOwnerUidAndTransportInfoNetCapsPermission( + false, /* shouldInclLocationSensitiveOwnerUidWithoutIncludeFlag */ + false, /* shouldInclLocationSensitiveOwnerUidWithIncludeFlag */ + false, /* shouldInclLocationSensitiveTransportInfoWithoutIncludeFlag */ + false /* shouldInclLocationSensitiveTransportInfoWithIncludeFlag */ + ); + } + + @Test + public void testCreateWithLocationInfoSanitizedLocationOff() throws Exception { + // Test that even with fine location permission, and UIDs matching, the UID is sanitized. + setupLocationPermissions(Build.VERSION_CODES.Q, false, AppOpsManager.OPSTR_FINE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION); + + verifyOwnerUidAndTransportInfoNetCapsNotIncluded(); + } + + @Test + public void testCreateWithLocationInfoSanitizedWrongUid() throws Exception { + // Test that even with fine location permission, not being the owner leads to sanitization. + setupLocationPermissions(Build.VERSION_CODES.Q, true, AppOpsManager.OPSTR_FINE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION); + + final int myUid = Process.myUid(); + assertEquals(Process.INVALID_UID, + getOwnerUidNetCapsPermission(myUid + 1, myUid, + true /* includeLocationSensitiveInfo */)); + } + + @Test + public void testCreateWithLocationInfoSanitizedWithCoarseLocationAfterQ() + throws Exception { + // Test that not having fine location permission leads to sanitization. + setupLocationPermissions(Build.VERSION_CODES.Q, true, AppOpsManager.OPSTR_COARSE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION); + + verifyOwnerUidAndTransportInfoNetCapsNotIncluded(); + } + + @Test + public void testCreateWithLocationInfoSanitizedWithCoarseLocationAfterS() + throws Exception { + // Test that not having fine location permission leads to sanitization. + setupLocationPermissions(Build.VERSION_CODES.S, true, AppOpsManager.OPSTR_COARSE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION); + + verifyOwnerUidAndTransportInfoNetCapsNotIncluded(); + } + + @Test + public void testCreateForCallerWithLocalMacAddressSanitizedWithLocalMacAddressPermission() + throws Exception { + mServiceContext.setPermission(Manifest.permission.LOCAL_MAC_ADDRESS, PERMISSION_GRANTED); + + final TransportInfo transportInfo = mock(TransportInfo.class); + when(transportInfo.getApplicableRedactions()) + .thenReturn(REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_LOCAL_MAC_ADDRESS); + final NetworkCapabilities netCap = + new NetworkCapabilities().setTransportInfo(transportInfo); + + mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled( + netCap, false /* includeLocationSensitiveInfoInTransportInfo */, + Process.myPid(), Process.myUid(), + mContext.getPackageName(), getAttributionTag()); + // don't redact MAC_ADDRESS fields, only location sensitive fields. + verify(transportInfo).makeCopy(REDACT_FOR_ACCESS_FINE_LOCATION); + } + + @Test + public void testCreateForCallerWithLocalMacAddressSanitizedWithoutLocalMacAddressPermission() + throws Exception { + mServiceContext.setPermission(Manifest.permission.LOCAL_MAC_ADDRESS, PERMISSION_DENIED); + + final TransportInfo transportInfo = mock(TransportInfo.class); + when(transportInfo.getApplicableRedactions()) + .thenReturn(REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_LOCAL_MAC_ADDRESS); + final NetworkCapabilities netCap = + new NetworkCapabilities().setTransportInfo(transportInfo); + + mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled( + netCap, false /* includeLocationSensitiveInfoInTransportInfo */, + Process.myPid(), Process.myUid(), + mContext.getPackageName(), getAttributionTag()); + // redact both MAC_ADDRESS & location sensitive fields. + verify(transportInfo).makeCopy(REDACT_FOR_ACCESS_FINE_LOCATION + | REDACT_FOR_LOCAL_MAC_ADDRESS); + } + + @Test + public void testCreateForCallerWithLocalMacAddressSanitizedWithSettingsPermission() + throws Exception { + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + + final TransportInfo transportInfo = mock(TransportInfo.class); + when(transportInfo.getApplicableRedactions()) + .thenReturn(REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_NETWORK_SETTINGS); + final NetworkCapabilities netCap = + new NetworkCapabilities().setTransportInfo(transportInfo); + + mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled( + netCap, false /* includeLocationSensitiveInfoInTransportInfo */, + Process.myPid(), Process.myUid(), + mContext.getPackageName(), getAttributionTag()); + // don't redact NETWORK_SETTINGS fields, only location sensitive fields. + verify(transportInfo).makeCopy(REDACT_FOR_ACCESS_FINE_LOCATION); + } + + @Test + public void testCreateForCallerWithLocalMacAddressSanitizedWithoutSettingsPermission() + throws Exception { + mServiceContext.setPermission(Manifest.permission.LOCAL_MAC_ADDRESS, PERMISSION_DENIED); + + final TransportInfo transportInfo = mock(TransportInfo.class); + when(transportInfo.getApplicableRedactions()) + .thenReturn(REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_NETWORK_SETTINGS); + final NetworkCapabilities netCap = + new NetworkCapabilities().setTransportInfo(transportInfo); + + mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled( + netCap, false /* includeLocationSensitiveInfoInTransportInfo */, + Process.myPid(), Process.myUid(), + mContext.getPackageName(), getAttributionTag()); + // redact both NETWORK_SETTINGS & location sensitive fields. + verify(transportInfo).makeCopy( + REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_NETWORK_SETTINGS); + } + + /** + * Test TransportInfo to verify redaction mechanism. + */ + private static class TestTransportInfo implements TransportInfo { + public final boolean locationRedacted; + public final boolean localMacAddressRedacted; + public final boolean settingsRedacted; + + TestTransportInfo() { + locationRedacted = false; + localMacAddressRedacted = false; + settingsRedacted = false; + } + + TestTransportInfo(boolean locationRedacted, boolean localMacAddressRedacted, + boolean settingsRedacted) { + this.locationRedacted = locationRedacted; + this.localMacAddressRedacted = + localMacAddressRedacted; + this.settingsRedacted = settingsRedacted; + } + + @Override + public TransportInfo makeCopy(@NetworkCapabilities.RedactionType long redactions) { + return new TestTransportInfo( + (redactions & REDACT_FOR_ACCESS_FINE_LOCATION) != 0, + (redactions & REDACT_FOR_LOCAL_MAC_ADDRESS) != 0, + (redactions & REDACT_FOR_NETWORK_SETTINGS) != 0 + ); + } + + @Override + public @NetworkCapabilities.RedactionType long getApplicableRedactions() { + return REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_LOCAL_MAC_ADDRESS + | REDACT_FOR_NETWORK_SETTINGS; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof TestTransportInfo)) return false; + TestTransportInfo that = (TestTransportInfo) other; + return that.locationRedacted == this.locationRedacted + && that.localMacAddressRedacted == this.localMacAddressRedacted + && that.settingsRedacted == this.settingsRedacted; + } + + @Override + public int hashCode() { + return Objects.hash(locationRedacted, localMacAddressRedacted, settingsRedacted); + } + } + + private void verifyNetworkCallbackLocationDataInclusionUsingTransportInfoAndOwnerUidInNetCaps( + @NonNull TestNetworkCallback wifiNetworkCallback, int actualOwnerUid, + @NonNull TransportInfo actualTransportInfo, int expectedOwnerUid, + @NonNull TransportInfo expectedTransportInfo) throws Exception { + when(mPackageManager.getTargetSdkVersion(anyString())).thenReturn(Build.VERSION_CODES.S); + final NetworkCapabilities ncTemplate = + new NetworkCapabilities() + .addTransportType(TRANSPORT_WIFI) + .setOwnerUid(actualOwnerUid); + + final NetworkRequest wifiRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).build(); + mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, new LinkProperties(), + ncTemplate); + mWiFiNetworkAgent.connect(false); + + wifiNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + + // Send network capabilities update with TransportInfo to trigger capabilities changed + // callback. + mWiFiNetworkAgent.setNetworkCapabilities( + ncTemplate.setTransportInfo(actualTransportInfo), true); + + wifiNetworkCallback.expectCapabilitiesThat(mWiFiNetworkAgent, + nc -> Objects.equals(expectedOwnerUid, nc.getOwnerUid()) + && Objects.equals(expectedTransportInfo, nc.getTransportInfo())); + + } + + @Test + public void testVerifyLocationDataIsNotIncludedWhenInclFlagNotSet() throws Exception { + final TestNetworkCallback wifiNetworkCallack = new TestNetworkCallback(); + final int ownerUid = Process.myUid(); + final TransportInfo transportInfo = new TestTransportInfo(); + // Even though the test uid holds privileged permissions, mask location fields since + // the callback did not explicitly opt-in to get location data. + final TransportInfo sanitizedTransportInfo = new TestTransportInfo( + true, /* locationRedacted */ + true, /* localMacAddressRedacted */ + true /* settingsRedacted */ + ); + // Should not expect location data since the callback does not set the flag for including + // location data. + verifyNetworkCallbackLocationDataInclusionUsingTransportInfoAndOwnerUidInNetCaps( + wifiNetworkCallack, ownerUid, transportInfo, INVALID_UID, sanitizedTransportInfo); + } + + private void setupConnectionOwnerUid(int vpnOwnerUid, @VpnManager.VpnType int vpnType) + throws Exception { + final Set vpnRange = Collections.singleton(PRIMARY_UIDRANGE); + mMockVpn.setVpnType(vpnType); + mMockVpn.establish(new LinkProperties(), vpnOwnerUid, vpnRange); + assertVpnUidRangesUpdated(true, vpnRange, vpnOwnerUid); + + final UnderlyingNetworkInfo underlyingNetworkInfo = + new UnderlyingNetworkInfo(vpnOwnerUid, VPN_IFNAME, new ArrayList()); + mMockVpn.setUnderlyingNetworkInfo(underlyingNetworkInfo); + when(mDeps.getConnectionOwnerUid(anyInt(), any(), any())).thenReturn(42); + } + + private void setupConnectionOwnerUidAsVpnApp(int vpnOwnerUid, @VpnManager.VpnType int vpnType) + throws Exception { + setupConnectionOwnerUid(vpnOwnerUid, vpnType); + + // Test as VPN app + mServiceContext.setPermission(android.Manifest.permission.NETWORK_STACK, PERMISSION_DENIED); + mServiceContext.setPermission( + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, PERMISSION_DENIED); + } + + private ConnectionInfo getTestConnectionInfo() throws Exception { + return new ConnectionInfo( + IPPROTO_TCP, + new InetSocketAddress(InetAddresses.parseNumericAddress("1.2.3.4"), 1234), + new InetSocketAddress(InetAddresses.parseNumericAddress("2.3.4.5"), 2345)); + } + + @Test + public void testGetConnectionOwnerUidPlatformVpn() throws Exception { + final int myUid = Process.myUid(); + setupConnectionOwnerUidAsVpnApp(myUid, VpnManager.TYPE_VPN_PLATFORM); + + assertEquals(INVALID_UID, mService.getConnectionOwnerUid(getTestConnectionInfo())); + } + + @Test + public void testGetConnectionOwnerUidVpnServiceWrongUser() throws Exception { + final int myUid = Process.myUid(); + setupConnectionOwnerUidAsVpnApp(myUid + 1, VpnManager.TYPE_VPN_SERVICE); + + assertEquals(INVALID_UID, mService.getConnectionOwnerUid(getTestConnectionInfo())); + } + + @Test + public void testGetConnectionOwnerUidVpnServiceDoesNotThrow() throws Exception { + final int myUid = Process.myUid(); + setupConnectionOwnerUidAsVpnApp(myUid, VpnManager.TYPE_VPN_SERVICE); + + assertEquals(42, mService.getConnectionOwnerUid(getTestConnectionInfo())); + } + + @Test + public void testGetConnectionOwnerUidVpnServiceNetworkStackDoesNotThrow() throws Exception { + final int myUid = Process.myUid(); + setupConnectionOwnerUid(myUid, VpnManager.TYPE_VPN_SERVICE); + mServiceContext.setPermission( + android.Manifest.permission.NETWORK_STACK, PERMISSION_GRANTED); + + assertEquals(42, mService.getConnectionOwnerUid(getTestConnectionInfo())); + } + + @Test + public void testGetConnectionOwnerUidVpnServiceMainlineNetworkStackDoesNotThrow() + throws Exception { + final int myUid = Process.myUid(); + setupConnectionOwnerUid(myUid, VpnManager.TYPE_VPN_SERVICE); + mServiceContext.setPermission( + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, PERMISSION_GRANTED); + + assertEquals(42, mService.getConnectionOwnerUid(getTestConnectionInfo())); + } + + private static PackageInfo buildPackageInfo(boolean hasSystemPermission, int uid) { + final PackageInfo packageInfo = new PackageInfo(); + if (hasSystemPermission) { + packageInfo.requestedPermissions = new String[] { + CHANGE_NETWORK_STATE, CONNECTIVITY_USE_RESTRICTED_NETWORKS }; + packageInfo.requestedPermissionsFlags = new int[] { + REQUESTED_PERMISSION_GRANTED, REQUESTED_PERMISSION_GRANTED }; + } else { + packageInfo.requestedPermissions = new String[0]; + } + packageInfo.applicationInfo = new ApplicationInfo(); + packageInfo.applicationInfo.privateFlags = 0; + packageInfo.applicationInfo.uid = UserHandle.getUid(UserHandle.USER_SYSTEM, + UserHandle.getAppId(uid)); + return packageInfo; + } + + @Test + public void testRegisterConnectivityDiagnosticsCallbackInvalidRequest() throws Exception { + final NetworkRequest request = + new NetworkRequest( + new NetworkCapabilities(), TYPE_ETHERNET, 0, NetworkRequest.Type.NONE); + try { + mService.registerConnectivityDiagnosticsCallback( + mConnectivityDiagnosticsCallback, request, mContext.getPackageName()); + fail("registerConnectivityDiagnosticsCallback should throw on invalid NetworkRequest"); + } catch (IllegalArgumentException expected) { + } + } + + private void assertRouteInfoParcelMatches(RouteInfo route, RouteInfoParcel parcel) { + assertEquals(route.getDestination().toString(), parcel.destination); + assertEquals(route.getInterface(), parcel.ifName); + assertEquals(route.getMtu(), parcel.mtu); + + switch (route.getType()) { + case RouteInfo.RTN_UNICAST: + if (route.hasGateway()) { + assertEquals(route.getGateway().getHostAddress(), parcel.nextHop); + } else { + assertEquals(INetd.NEXTHOP_NONE, parcel.nextHop); + } + break; + case RouteInfo.RTN_UNREACHABLE: + assertEquals(INetd.NEXTHOP_UNREACHABLE, parcel.nextHop); + break; + case RouteInfo.RTN_THROW: + assertEquals(INetd.NEXTHOP_THROW, parcel.nextHop); + break; + default: + assertEquals(INetd.NEXTHOP_NONE, parcel.nextHop); + break; + } + } + + private void assertRoutesAdded(int netId, RouteInfo... routes) throws Exception { + ArgumentCaptor captor = ArgumentCaptor.forClass(RouteInfoParcel.class); + verify(mMockNetd, times(routes.length)).networkAddRouteParcel(eq(netId), captor.capture()); + for (int i = 0; i < routes.length; i++) { + assertRouteInfoParcelMatches(routes[i], captor.getAllValues().get(i)); + } + } + + private void assertRoutesRemoved(int netId, RouteInfo... routes) throws Exception { + ArgumentCaptor captor = ArgumentCaptor.forClass(RouteInfoParcel.class); + verify(mMockNetd, times(routes.length)).networkRemoveRouteParcel(eq(netId), + captor.capture()); + for (int i = 0; i < routes.length; i++) { + assertRouteInfoParcelMatches(routes[i], captor.getAllValues().get(i)); + } + } + + @Test + public void testRegisterUnregisterConnectivityDiagnosticsCallback() throws Exception { + final NetworkRequest wifiRequest = + new NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(); + when(mConnectivityDiagnosticsCallback.asBinder()).thenReturn(mIBinder); + + mService.registerConnectivityDiagnosticsCallback( + mConnectivityDiagnosticsCallback, wifiRequest, mContext.getPackageName()); + + // Block until all other events are done processing. + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + + verify(mIBinder).linkToDeath(any(ConnectivityDiagnosticsCallbackInfo.class), anyInt()); + verify(mConnectivityDiagnosticsCallback).asBinder(); + assertTrue(mService.mConnectivityDiagnosticsCallbacks.containsKey(mIBinder)); + + mService.unregisterConnectivityDiagnosticsCallback(mConnectivityDiagnosticsCallback); + verify(mIBinder, timeout(TIMEOUT_MS)) + .unlinkToDeath(any(ConnectivityDiagnosticsCallbackInfo.class), anyInt()); + assertFalse(mService.mConnectivityDiagnosticsCallbacks.containsKey(mIBinder)); + verify(mConnectivityDiagnosticsCallback, atLeastOnce()).asBinder(); + } + + @Test + public void testRegisterDuplicateConnectivityDiagnosticsCallback() throws Exception { + final NetworkRequest wifiRequest = + new NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(); + when(mConnectivityDiagnosticsCallback.asBinder()).thenReturn(mIBinder); + + mService.registerConnectivityDiagnosticsCallback( + mConnectivityDiagnosticsCallback, wifiRequest, mContext.getPackageName()); + + // Block until all other events are done processing. + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + + verify(mIBinder).linkToDeath(any(ConnectivityDiagnosticsCallbackInfo.class), anyInt()); + verify(mConnectivityDiagnosticsCallback).asBinder(); + assertTrue(mService.mConnectivityDiagnosticsCallbacks.containsKey(mIBinder)); + + // Register the same callback again + mService.registerConnectivityDiagnosticsCallback( + mConnectivityDiagnosticsCallback, wifiRequest, mContext.getPackageName()); + + // Block until all other events are done processing. + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + + assertTrue(mService.mConnectivityDiagnosticsCallbacks.containsKey(mIBinder)); + } + + public NetworkAgentInfo fakeMobileNai(NetworkCapabilities nc) { + final NetworkInfo info = new NetworkInfo(TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_LTE, + ConnectivityManager.getNetworkTypeName(TYPE_MOBILE), + TelephonyManager.getNetworkTypeName(TelephonyManager.NETWORK_TYPE_LTE)); + return new NetworkAgentInfo(null, new Network(NET_ID), info, new LinkProperties(), + nc, new NetworkScore.Builder().setLegacyInt(0).build(), + mServiceContext, null, new NetworkAgentConfig(), mService, null, null, 0, + INVALID_UID, mQosCallbackTracker, new ConnectivityService.Dependencies()); + } + + @Test + public void testCheckConnectivityDiagnosticsPermissionsNetworkStack() throws Exception { + final NetworkAgentInfo naiWithoutUid = fakeMobileNai(new NetworkCapabilities()); + + mServiceContext.setPermission( + android.Manifest.permission.NETWORK_STACK, PERMISSION_GRANTED); + assertTrue( + "NetworkStack permission not applied", + mService.checkConnectivityDiagnosticsPermissions( + Process.myPid(), Process.myUid(), naiWithoutUid, + mContext.getOpPackageName())); + } + + @Test + public void testCheckConnectivityDiagnosticsPermissionsWrongUidPackageName() throws Exception { + final NetworkAgentInfo naiWithoutUid = fakeMobileNai(new NetworkCapabilities()); + + mServiceContext.setPermission(android.Manifest.permission.NETWORK_STACK, PERMISSION_DENIED); + + assertFalse( + "Mismatched uid/package name should not pass the location permission check", + mService.checkConnectivityDiagnosticsPermissions( + Process.myPid() + 1, Process.myUid() + 1, naiWithoutUid, + mContext.getOpPackageName())); + } + + @Test + public void testCheckConnectivityDiagnosticsPermissionsNoLocationPermission() throws Exception { + final NetworkAgentInfo naiWithoutUid = fakeMobileNai(new NetworkCapabilities()); + + mServiceContext.setPermission(android.Manifest.permission.NETWORK_STACK, PERMISSION_DENIED); + + assertFalse( + "ACCESS_FINE_LOCATION permission necessary for Connectivity Diagnostics", + mService.checkConnectivityDiagnosticsPermissions( + Process.myPid(), Process.myUid(), naiWithoutUid, + mContext.getOpPackageName())); + } + + @Test + public void testCheckConnectivityDiagnosticsPermissionsActiveVpn() throws Exception { + final NetworkAgentInfo naiWithoutUid = fakeMobileNai(new NetworkCapabilities()); + + mMockVpn.establishForMyUid(); + assertUidRangesUpdatedForMyUid(true); + + // Wait for networks to connect and broadcasts to be sent before removing permissions. + waitForIdle(); + setupLocationPermissions(Build.VERSION_CODES.Q, true, AppOpsManager.OPSTR_FINE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION); + + assertTrue(mMockVpn.setUnderlyingNetworks(new Network[] {naiWithoutUid.network})); + waitForIdle(); + assertTrue( + "Active VPN permission not applied", + mService.checkConnectivityDiagnosticsPermissions( + Process.myPid(), Process.myUid(), naiWithoutUid, + mContext.getOpPackageName())); + + assertTrue(mMockVpn.setUnderlyingNetworks(null)); + waitForIdle(); + assertFalse( + "VPN shouldn't receive callback on non-underlying network", + mService.checkConnectivityDiagnosticsPermissions( + Process.myPid(), Process.myUid(), naiWithoutUid, + mContext.getOpPackageName())); + } + + @Test + public void testCheckConnectivityDiagnosticsPermissionsNetworkAdministrator() throws Exception { + final NetworkCapabilities nc = new NetworkCapabilities(); + nc.setAdministratorUids(new int[] {Process.myUid()}); + final NetworkAgentInfo naiWithUid = fakeMobileNai(nc); + + setupLocationPermissions(Build.VERSION_CODES.Q, true, AppOpsManager.OPSTR_FINE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION); + mServiceContext.setPermission(android.Manifest.permission.NETWORK_STACK, PERMISSION_DENIED); + + assertTrue( + "NetworkCapabilities administrator uid permission not applied", + mService.checkConnectivityDiagnosticsPermissions( + Process.myPid(), Process.myUid(), naiWithUid, mContext.getOpPackageName())); + } + + @Test + public void testCheckConnectivityDiagnosticsPermissionsFails() throws Exception { + final NetworkCapabilities nc = new NetworkCapabilities(); + nc.setOwnerUid(Process.myUid()); + nc.setAdministratorUids(new int[] {Process.myUid()}); + final NetworkAgentInfo naiWithUid = fakeMobileNai(nc); + + setupLocationPermissions(Build.VERSION_CODES.Q, true, AppOpsManager.OPSTR_FINE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION); + mServiceContext.setPermission(android.Manifest.permission.NETWORK_STACK, PERMISSION_DENIED); + + // Use wrong pid and uid + assertFalse( + "Permissions allowed when they shouldn't be granted", + mService.checkConnectivityDiagnosticsPermissions( + Process.myPid() + 1, Process.myUid() + 1, naiWithUid, + mContext.getOpPackageName())); + } + + @Test + public void testRegisterConnectivityDiagnosticsCallbackCallsOnConnectivityReport() + throws Exception { + // Set up the Network, which leads to a ConnectivityReport being cached for the network. + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(callback); + final LinkProperties linkProperties = new LinkProperties(); + linkProperties.setInterfaceName(INTERFACE_NAME); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, linkProperties); + mCellNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + callback.assertNoCallback(); + + final NetworkRequest request = new NetworkRequest.Builder().build(); + when(mConnectivityDiagnosticsCallback.asBinder()).thenReturn(mIBinder); + + mServiceContext.setPermission( + android.Manifest.permission.NETWORK_STACK, PERMISSION_GRANTED); + + mService.registerConnectivityDiagnosticsCallback( + mConnectivityDiagnosticsCallback, request, mContext.getPackageName()); + + // Block until all other events are done processing. + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + + verify(mConnectivityDiagnosticsCallback) + .onConnectivityReportAvailable(argThat(report -> { + return INTERFACE_NAME.equals(report.getLinkProperties().getInterfaceName()) + && report.getNetworkCapabilities().hasTransport(TRANSPORT_CELLULAR); + })); + } + + private void setUpConnectivityDiagnosticsCallback() throws Exception { + final NetworkRequest request = new NetworkRequest.Builder().build(); + when(mConnectivityDiagnosticsCallback.asBinder()).thenReturn(mIBinder); + + mServiceContext.setPermission( + android.Manifest.permission.NETWORK_STACK, PERMISSION_GRANTED); + + mService.registerConnectivityDiagnosticsCallback( + mConnectivityDiagnosticsCallback, request, mContext.getPackageName()); + + // Block until all other events are done processing. + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + + // Connect the cell agent verify that it notifies TestNetworkCallback that it is available + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(callback); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + callback.assertNoCallback(); + } + + @Test + public void testConnectivityDiagnosticsCallbackOnConnectivityReportAvailable() + throws Exception { + setUpConnectivityDiagnosticsCallback(); + + // Block until all other events are done processing. + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + + // Verify onConnectivityReport fired + verify(mConnectivityDiagnosticsCallback).onConnectivityReportAvailable( + argThat(report -> { + final NetworkCapabilities nc = report.getNetworkCapabilities(); + return nc.getUids() == null + && nc.getAdministratorUids().length == 0 + && nc.getOwnerUid() == Process.INVALID_UID; + })); + } + + @Test + public void testConnectivityDiagnosticsCallbackOnDataStallSuspected() throws Exception { + setUpConnectivityDiagnosticsCallback(); + + // Trigger notifyDataStallSuspected() on the INetworkMonitorCallbacks instance in the + // cellular network agent + mCellNetworkAgent.notifyDataStallSuspected(); + + // Block until all other events are done processing. + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + + // Verify onDataStallSuspected fired + verify(mConnectivityDiagnosticsCallback).onDataStallSuspected( + argThat(report -> { + final NetworkCapabilities nc = report.getNetworkCapabilities(); + return nc.getUids() == null + && nc.getAdministratorUids().length == 0 + && nc.getOwnerUid() == Process.INVALID_UID; + })); + } + + @Test + public void testConnectivityDiagnosticsCallbackOnConnectivityReported() throws Exception { + setUpConnectivityDiagnosticsCallback(); + + final Network n = mCellNetworkAgent.getNetwork(); + final boolean hasConnectivity = true; + mService.reportNetworkConnectivity(n, hasConnectivity); + + // Block until all other events are done processing. + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + + // Verify onNetworkConnectivityReported fired + verify(mConnectivityDiagnosticsCallback) + .onNetworkConnectivityReported(eq(n), eq(hasConnectivity)); + + final boolean noConnectivity = false; + mService.reportNetworkConnectivity(n, noConnectivity); + + // Block until all other events are done processing. + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + + // Wait for onNetworkConnectivityReported to fire + verify(mConnectivityDiagnosticsCallback) + .onNetworkConnectivityReported(eq(n), eq(noConnectivity)); + } + + @Test + public void testRouteAddDeleteUpdate() throws Exception { + final NetworkRequest request = new NetworkRequest.Builder().build(); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, networkCallback); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + reset(mMockNetd); + mCellNetworkAgent.connect(false); + networkCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + final int netId = mCellNetworkAgent.getNetwork().netId; + + final String iface = "rmnet_data0"; + final InetAddress gateway = InetAddress.getByName("fe80::5678"); + RouteInfo direct = RouteInfo.makeHostRoute(gateway, iface); + RouteInfo rio1 = new RouteInfo(new IpPrefix("2001:db8:1::/48"), gateway, iface); + RouteInfo rio2 = new RouteInfo(new IpPrefix("2001:db8:2::/48"), gateway, iface); + RouteInfo defaultRoute = new RouteInfo((IpPrefix) null, gateway, iface); + RouteInfo defaultWithMtu = new RouteInfo(null, gateway, iface, RouteInfo.RTN_UNICAST, + 1280 /* mtu */); + + // Send LinkProperties and check that we ask netd to add routes. + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(iface); + lp.addRoute(direct); + lp.addRoute(rio1); + lp.addRoute(defaultRoute); + mCellNetworkAgent.sendLinkProperties(lp); + networkCallback.expectLinkPropertiesThat(mCellNetworkAgent, x -> x.getRoutes().size() == 3); + + assertRoutesAdded(netId, direct, rio1, defaultRoute); + reset(mMockNetd); + + // Send updated LinkProperties and check that we ask netd to add, remove, update routes. + assertTrue(lp.getRoutes().contains(defaultRoute)); + lp.removeRoute(rio1); + lp.addRoute(rio2); + lp.addRoute(defaultWithMtu); + // Ensure adding the same route with a different MTU replaces the previous route. + assertFalse(lp.getRoutes().contains(defaultRoute)); + assertTrue(lp.getRoutes().contains(defaultWithMtu)); + + mCellNetworkAgent.sendLinkProperties(lp); + networkCallback.expectLinkPropertiesThat(mCellNetworkAgent, + x -> x.getRoutes().contains(rio2)); + + assertRoutesRemoved(netId, rio1); + assertRoutesAdded(netId, rio2); + + ArgumentCaptor captor = ArgumentCaptor.forClass(RouteInfoParcel.class); + verify(mMockNetd).networkUpdateRouteParcel(eq(netId), captor.capture()); + assertRouteInfoParcelMatches(defaultWithMtu, captor.getValue()); + + + mCm.unregisterNetworkCallback(networkCallback); + } + + @Test + public void testDumpDoesNotCrash() { + mServiceContext.setPermission(DUMP, PERMISSION_GRANTED); + // Filing a couple requests prior to testing the dump. + final TestNetworkCallback genericNetworkCallback = new TestNetworkCallback(); + final TestNetworkCallback wifiNetworkCallback = new TestNetworkCallback(); + final NetworkRequest genericRequest = new NetworkRequest.Builder() + .clearCapabilities().build(); + final NetworkRequest wifiRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).build(); + mCm.registerNetworkCallback(genericRequest, genericNetworkCallback); + mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback); + final StringWriter stringWriter = new StringWriter(); + + mService.dump(new FileDescriptor(), new PrintWriter(stringWriter), new String[0]); + + assertFalse(stringWriter.toString().isEmpty()); + } + + @Test + public void testRequestsSortedByIdSortsCorrectly() { + final TestNetworkCallback genericNetworkCallback = new TestNetworkCallback(); + final TestNetworkCallback wifiNetworkCallback = new TestNetworkCallback(); + final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback(); + final NetworkRequest genericRequest = new NetworkRequest.Builder() + .clearCapabilities().build(); + final NetworkRequest wifiRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).build(); + final NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + mCm.registerNetworkCallback(genericRequest, genericNetworkCallback); + mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback); + mCm.registerNetworkCallback(cellRequest, cellNetworkCallback); + waitForIdle(); + + final ConnectivityService.NetworkRequestInfo[] nriOutput = mService.requestsSortedById(); + + assertTrue(nriOutput.length > 1); + for (int i = 0; i < nriOutput.length - 1; i++) { + final boolean isRequestIdInOrder = + nriOutput[i].mRequests.get(0).requestId + < nriOutput[i + 1].mRequests.get(0).requestId; + assertTrue(isRequestIdInOrder); + } + } + + private void assertUidRangesUpdatedForMyUid(boolean add) throws Exception { + final int uid = Process.myUid(); + assertVpnUidRangesUpdated(add, uidRangesForUids(uid), uid); + } + + private void assertVpnUidRangesUpdated(boolean add, Set vpnRanges, int exemptUid) + throws Exception { + InOrder inOrder = inOrder(mMockNetd); + ArgumentCaptor exemptUidCaptor = ArgumentCaptor.forClass(int[].class); + + inOrder.verify(mMockNetd, times(1)).socketDestroy(eq(toUidRangeStableParcels(vpnRanges)), + exemptUidCaptor.capture()); + assertContainsExactly(exemptUidCaptor.getValue(), Process.VPN_UID, exemptUid); + + if (add) { + inOrder.verify(mMockNetd, times(1)) + .networkAddUidRanges(eq(mMockVpn.getNetwork().getNetId()), + eq(toUidRangeStableParcels(vpnRanges))); + } else { + inOrder.verify(mMockNetd, times(1)) + .networkRemoveUidRanges(eq(mMockVpn.getNetwork().getNetId()), + eq(toUidRangeStableParcels(vpnRanges))); + } + + inOrder.verify(mMockNetd, times(1)).socketDestroy(eq(toUidRangeStableParcels(vpnRanges)), + exemptUidCaptor.capture()); + assertContainsExactly(exemptUidCaptor.getValue(), Process.VPN_UID, exemptUid); + } + + @Test + public void testVpnUidRangesUpdate() throws Exception { + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("tun0"); + lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null)); + lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null)); + final UidRange vpnRange = PRIMARY_UIDRANGE; + Set vpnRanges = Collections.singleton(vpnRange); + mMockVpn.establish(lp, VPN_UID, vpnRanges); + assertVpnUidRangesUpdated(true, vpnRanges, VPN_UID); + + reset(mMockNetd); + // Update to new range which is old range minus APP1, i.e. only APP2 + final Set newRanges = new HashSet<>(Arrays.asList( + new UidRange(vpnRange.start, APP1_UID - 1), + new UidRange(APP1_UID + 1, vpnRange.stop))); + mMockVpn.setUids(newRanges); + waitForIdle(); + + assertVpnUidRangesUpdated(true, newRanges, VPN_UID); + assertVpnUidRangesUpdated(false, vpnRanges, VPN_UID); + } + + @Test + public void testInvalidRequestTypes() { + final int[] invalidReqTypeInts = new int[]{-1, NetworkRequest.Type.NONE.ordinal(), + NetworkRequest.Type.LISTEN.ordinal(), NetworkRequest.Type.values().length}; + final NetworkCapabilities nc = new NetworkCapabilities().addTransportType(TRANSPORT_WIFI); + + for (int reqTypeInt : invalidReqTypeInts) { + assertThrows("Expect throws for invalid request type " + reqTypeInt, + IllegalArgumentException.class, + () -> mService.requestNetwork(Process.INVALID_UID, nc, reqTypeInt, null, 0, + null, ConnectivityManager.TYPE_NONE, NetworkCallback.FLAG_NONE, + mContext.getPackageName(), getAttributionTag()) + ); + } + } + + private class QosCallbackMockHelper { + @NonNull public final QosFilter mFilter; + @NonNull public final IQosCallback mCallback; + @NonNull public final TestNetworkAgentWrapper mAgentWrapper; + @NonNull private final List mCallbacks = new ArrayList(); + + QosCallbackMockHelper() throws Exception { + Log.d(TAG, "QosCallbackMockHelper: "); + mFilter = mock(QosFilter.class); + + // Ensure the network is disconnected before anything else occurs + assertNull(mCellNetworkAgent); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + + verifyActiveNetwork(TRANSPORT_CELLULAR); + waitForIdle(); + final Network network = mCellNetworkAgent.getNetwork(); + + final Pair pair = createQosCallback(); + mCallback = pair.first; + + when(mFilter.getNetwork()).thenReturn(network); + when(mFilter.validate()).thenReturn(QosCallbackException.EX_TYPE_FILTER_NONE); + mAgentWrapper = mCellNetworkAgent; + } + + void registerQosCallback(@NonNull final QosFilter filter, + @NonNull final IQosCallback callback) { + mCallbacks.add(callback); + final NetworkAgentInfo nai = + mService.getNetworkAgentInfoForNetwork(filter.getNetwork()); + mService.registerQosCallbackInternal(filter, callback, nai); + } + + void tearDown() { + for (int i = 0; i < mCallbacks.size(); i++) { + mService.unregisterQosCallback(mCallbacks.get(i)); + } + } + } + + private Pair createQosCallback() { + final IQosCallback callback = mock(IQosCallback.class); + final IBinder binder = mock(Binder.class); + when(callback.asBinder()).thenReturn(binder); + when(binder.isBinderAlive()).thenReturn(true); + return new Pair<>(callback, binder); + } + + + @Test + public void testQosCallbackRegistration() throws Exception { + mQosCallbackMockHelper = new QosCallbackMockHelper(); + final NetworkAgentWrapper wrapper = mQosCallbackMockHelper.mAgentWrapper; + + when(mQosCallbackMockHelper.mFilter.validate()) + .thenReturn(QosCallbackException.EX_TYPE_FILTER_NONE); + mQosCallbackMockHelper.registerQosCallback( + mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback); + + final NetworkAgentWrapper.CallbackType.OnQosCallbackRegister cbRegister1 = + (NetworkAgentWrapper.CallbackType.OnQosCallbackRegister) + wrapper.getCallbackHistory().poll(1000, x -> true); + assertNotNull(cbRegister1); + + final int registerCallbackId = cbRegister1.mQosCallbackId; + mService.unregisterQosCallback(mQosCallbackMockHelper.mCallback); + final NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister cbUnregister; + cbUnregister = (NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister) + wrapper.getCallbackHistory().poll(1000, x -> true); + assertNotNull(cbUnregister); + assertEquals(registerCallbackId, cbUnregister.mQosCallbackId); + assertNull(wrapper.getCallbackHistory().poll(200, x -> true)); + } + + @Test + public void testQosCallbackNoRegistrationOnValidationError() throws Exception { + mQosCallbackMockHelper = new QosCallbackMockHelper(); + + when(mQosCallbackMockHelper.mFilter.validate()) + .thenReturn(QosCallbackException.EX_TYPE_FILTER_NETWORK_RELEASED); + mQosCallbackMockHelper.registerQosCallback( + mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback); + waitForIdle(); + verify(mQosCallbackMockHelper.mCallback) + .onError(eq(QosCallbackException.EX_TYPE_FILTER_NETWORK_RELEASED)); + } + + @Test + public void testQosCallbackAvailableAndLost() throws Exception { + mQosCallbackMockHelper = new QosCallbackMockHelper(); + final int sessionId = 10; + final int qosCallbackId = 1; + + when(mQosCallbackMockHelper.mFilter.validate()) + .thenReturn(QosCallbackException.EX_TYPE_FILTER_NONE); + mQosCallbackMockHelper.registerQosCallback( + mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback); + waitForIdle(); + + final EpsBearerQosSessionAttributes attributes = new EpsBearerQosSessionAttributes( + 1, 2, 3, 4, 5, new ArrayList<>()); + mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent() + .sendQosSessionAvailable(qosCallbackId, sessionId, attributes); + waitForIdle(); + + verify(mQosCallbackMockHelper.mCallback).onQosEpsBearerSessionAvailable(argThat(session -> + session.getSessionId() == sessionId + && session.getSessionType() == QosSession.TYPE_EPS_BEARER), eq(attributes)); + + mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent() + .sendQosSessionLost(qosCallbackId, sessionId, QosSession.TYPE_EPS_BEARER); + waitForIdle(); + verify(mQosCallbackMockHelper.mCallback).onQosSessionLost(argThat(session -> + session.getSessionId() == sessionId + && session.getSessionType() == QosSession.TYPE_EPS_BEARER)); + } + + @Test + public void testNrQosCallbackAvailableAndLost() throws Exception { + mQosCallbackMockHelper = new QosCallbackMockHelper(); + final int sessionId = 10; + final int qosCallbackId = 1; + + when(mQosCallbackMockHelper.mFilter.validate()) + .thenReturn(QosCallbackException.EX_TYPE_FILTER_NONE); + mQosCallbackMockHelper.registerQosCallback( + mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback); + waitForIdle(); + + final NrQosSessionAttributes attributes = new NrQosSessionAttributes( + 1, 2, 3, 4, 5, 6, 7, new ArrayList<>()); + mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent() + .sendQosSessionAvailable(qosCallbackId, sessionId, attributes); + waitForIdle(); + + verify(mQosCallbackMockHelper.mCallback).onNrQosSessionAvailable(argThat(session -> + session.getSessionId() == sessionId + && session.getSessionType() == QosSession.TYPE_NR_BEARER), eq(attributes)); + + mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent() + .sendQosSessionLost(qosCallbackId, sessionId, QosSession.TYPE_NR_BEARER); + waitForIdle(); + verify(mQosCallbackMockHelper.mCallback).onQosSessionLost(argThat(session -> + session.getSessionId() == sessionId + && session.getSessionType() == QosSession.TYPE_NR_BEARER)); + } + + @Test + public void testQosCallbackTooManyRequests() throws Exception { + mQosCallbackMockHelper = new QosCallbackMockHelper(); + + when(mQosCallbackMockHelper.mFilter.validate()) + .thenReturn(QosCallbackException.EX_TYPE_FILTER_NONE); + for (int i = 0; i < 100; i++) { + final Pair pair = createQosCallback(); + + try { + mQosCallbackMockHelper.registerQosCallback( + mQosCallbackMockHelper.mFilter, pair.first); + } catch (ServiceSpecificException e) { + assertEquals(e.errorCode, ConnectivityManager.Errors.TOO_MANY_REQUESTS); + if (i < 50) { + fail("TOO_MANY_REQUESTS thrown too early, the count is " + i); + } + + // As long as there is at least 50 requests, it is safe to assume it works. + // Note: The count isn't being tested precisely against 100 because the counter + // is shared with request network. + return; + } + } + fail("TOO_MANY_REQUESTS never thrown"); + } + + private UidRange createUidRange(int userId) { + return UidRange.createForUser(UserHandle.of(userId)); + } + + private void mockGetApplicationInfo(@NonNull final String packageName, @NonNull final int uid) { + final ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.uid = uid; + try { + when(mPackageManager.getApplicationInfo(eq(packageName), anyInt())) + .thenReturn(applicationInfo); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + private void mockGetApplicationInfoThrowsNameNotFound(@NonNull final String packageName) + throws Exception { + when(mPackageManager.getApplicationInfo(eq(packageName), anyInt())) + .thenThrow(new PackageManager.NameNotFoundException(packageName)); + } + + private void mockHasSystemFeature(@NonNull final String featureName, + @NonNull final boolean hasFeature) { + when(mPackageManager.hasSystemFeature(eq(featureName))) + .thenReturn(hasFeature); + } + + private Range getNriFirstUidRange( + @NonNull final ConnectivityService.NetworkRequestInfo nri) { + return nri.mRequests.get(0).networkCapabilities.getUids().iterator().next(); + } + + private OemNetworkPreferences createDefaultOemNetworkPreferences( + @OemNetworkPreferences.OemNetworkPreference final int preference) { + // Arrange PackageManager mocks + mockGetApplicationInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_UID); + + // Build OemNetworkPreferences object + return new OemNetworkPreferences.Builder() + .addNetworkPreference(TEST_PACKAGE_NAME, preference) + .build(); + } + + @Test + public void testOemNetworkRequestFactoryPreferenceUninitializedThrowsError() + throws PackageManager.NameNotFoundException { + @OemNetworkPreferences.OemNetworkPreference final int prefToTest = + OEM_NETWORK_PREFERENCE_UNINITIALIZED; + + // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences() + assertThrows(IllegalArgumentException.class, + () -> mService.new OemNetworkRequestFactory() + .createNrisFromOemNetworkPreferences( + createDefaultOemNetworkPreferences(prefToTest))); + } + + @Test + public void testOemNetworkRequestFactoryPreferenceOemPaid() + throws Exception { + // Expectations + final int expectedNumOfNris = 1; + final int expectedNumOfRequests = 3; + + @OemNetworkPreferences.OemNetworkPreference final int prefToTest = + OEM_NETWORK_PREFERENCE_OEM_PAID; + + // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences() + final ArraySet nris = + mService.new OemNetworkRequestFactory() + .createNrisFromOemNetworkPreferences( + createDefaultOemNetworkPreferences(prefToTest)); + + final List mRequests = nris.iterator().next().mRequests; + assertEquals(expectedNumOfNris, nris.size()); + assertEquals(expectedNumOfRequests, mRequests.size()); + assertTrue(mRequests.get(0).isListen()); + assertTrue(mRequests.get(0).hasCapability(NET_CAPABILITY_NOT_METERED)); + assertTrue(mRequests.get(0).hasCapability(NET_CAPABILITY_VALIDATED)); + assertTrue(mRequests.get(1).isRequest()); + assertTrue(mRequests.get(1).hasCapability(NET_CAPABILITY_OEM_PAID)); + assertEquals(NetworkRequest.Type.TRACK_DEFAULT, mRequests.get(2).type); + assertTrue(mService.getDefaultRequest().networkCapabilities.equalsNetCapabilities( + mRequests.get(2).networkCapabilities)); + } + + @Test + public void testOemNetworkRequestFactoryPreferenceOemPaidNoFallback() + throws Exception { + // Expectations + final int expectedNumOfNris = 1; + final int expectedNumOfRequests = 2; + + @OemNetworkPreferences.OemNetworkPreference final int prefToTest = + OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK; + + // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences() + final ArraySet nris = + mService.new OemNetworkRequestFactory() + .createNrisFromOemNetworkPreferences( + createDefaultOemNetworkPreferences(prefToTest)); + + final List mRequests = nris.iterator().next().mRequests; + assertEquals(expectedNumOfNris, nris.size()); + assertEquals(expectedNumOfRequests, mRequests.size()); + assertTrue(mRequests.get(0).isListen()); + assertTrue(mRequests.get(0).hasCapability(NET_CAPABILITY_NOT_METERED)); + assertTrue(mRequests.get(0).hasCapability(NET_CAPABILITY_VALIDATED)); + assertTrue(mRequests.get(1).isRequest()); + assertTrue(mRequests.get(1).hasCapability(NET_CAPABILITY_OEM_PAID)); + } + + @Test + public void testOemNetworkRequestFactoryPreferenceOemPaidOnly() + throws Exception { + // Expectations + final int expectedNumOfNris = 1; + final int expectedNumOfRequests = 1; + + @OemNetworkPreferences.OemNetworkPreference final int prefToTest = + OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY; + + // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences() + final ArraySet nris = + mService.new OemNetworkRequestFactory() + .createNrisFromOemNetworkPreferences( + createDefaultOemNetworkPreferences(prefToTest)); + + final List mRequests = nris.iterator().next().mRequests; + assertEquals(expectedNumOfNris, nris.size()); + assertEquals(expectedNumOfRequests, mRequests.size()); + assertTrue(mRequests.get(0).isRequest()); + assertTrue(mRequests.get(0).hasCapability(NET_CAPABILITY_OEM_PAID)); + } + + @Test + public void testOemNetworkRequestFactoryPreferenceOemPrivateOnly() + throws Exception { + // Expectations + final int expectedNumOfNris = 1; + final int expectedNumOfRequests = 1; + + @OemNetworkPreferences.OemNetworkPreference final int prefToTest = + OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY; + + // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences() + final ArraySet nris = + mService.new OemNetworkRequestFactory() + .createNrisFromOemNetworkPreferences( + createDefaultOemNetworkPreferences(prefToTest)); + + final List mRequests = nris.iterator().next().mRequests; + assertEquals(expectedNumOfNris, nris.size()); + assertEquals(expectedNumOfRequests, mRequests.size()); + assertTrue(mRequests.get(0).isRequest()); + assertTrue(mRequests.get(0).hasCapability(NET_CAPABILITY_OEM_PRIVATE)); + assertFalse(mRequests.get(0).hasCapability(NET_CAPABILITY_OEM_PAID)); + } + + @Test + public void testOemNetworkRequestFactoryCreatesCorrectNumOfNris() + throws Exception { + // Expectations + final int expectedNumOfNris = 2; + + // Arrange PackageManager mocks + final String testPackageName2 = "com.google.apps.dialer"; + mockGetApplicationInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_UID); + mockGetApplicationInfo(testPackageName2, TEST_PACKAGE_UID); + + // Build OemNetworkPreferences object + final int testOemPref = OEM_NETWORK_PREFERENCE_OEM_PAID; + final int testOemPref2 = OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK; + final OemNetworkPreferences pref = new OemNetworkPreferences.Builder() + .addNetworkPreference(TEST_PACKAGE_NAME, testOemPref) + .addNetworkPreference(testPackageName2, testOemPref2) + .build(); + + // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences() + final ArraySet nris = + mService.new OemNetworkRequestFactory().createNrisFromOemNetworkPreferences(pref); + + assertNotNull(nris); + assertEquals(expectedNumOfNris, nris.size()); + } + + @Test + public void testOemNetworkRequestFactoryMultiplePrefsCorrectlySetsUids() + throws Exception { + // Arrange PackageManager mocks + final String testPackageName2 = "com.google.apps.dialer"; + final int testPackageNameUid2 = 456; + mockGetApplicationInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_UID); + mockGetApplicationInfo(testPackageName2, testPackageNameUid2); + + // Build OemNetworkPreferences object + final int testOemPref = OEM_NETWORK_PREFERENCE_OEM_PAID; + final int testOemPref2 = OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK; + final OemNetworkPreferences pref = new OemNetworkPreferences.Builder() + .addNetworkPreference(TEST_PACKAGE_NAME, testOemPref) + .addNetworkPreference(testPackageName2, testOemPref2) + .build(); + + // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences() + final List nris = + new ArrayList<>( + mService.new OemNetworkRequestFactory().createNrisFromOemNetworkPreferences( + pref)); + + // Sort by uid to access nris by index + nris.sort(Comparator.comparingInt(nri -> getNriFirstUidRange(nri).getLower())); + assertEquals(TEST_PACKAGE_UID, (int) getNriFirstUidRange(nris.get(0)).getLower()); + assertEquals(TEST_PACKAGE_UID, (int) getNriFirstUidRange(nris.get(0)).getUpper()); + assertEquals(testPackageNameUid2, (int) getNriFirstUidRange(nris.get(1)).getLower()); + assertEquals(testPackageNameUid2, (int) getNriFirstUidRange(nris.get(1)).getUpper()); + } + + @Test + public void testOemNetworkRequestFactoryMultipleUsersCorrectlySetsUids() + throws Exception { + // Arrange users + final int secondUser = 10; + final UserHandle secondUserHandle = new UserHandle(secondUser); + when(mUserManager.getUserHandles(anyBoolean())).thenReturn( + Arrays.asList(PRIMARY_USER_HANDLE, secondUserHandle)); + + // Arrange PackageManager mocks + mockGetApplicationInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_UID); + + // Build OemNetworkPreferences object + final int testOemPref = OEM_NETWORK_PREFERENCE_OEM_PAID; + final OemNetworkPreferences pref = new OemNetworkPreferences.Builder() + .addNetworkPreference(TEST_PACKAGE_NAME, testOemPref) + .build(); + + // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences() + final List nris = + new ArrayList<>( + mService.new OemNetworkRequestFactory().createNrisFromOemNetworkPreferences( + pref)); + + // UIDs for all users and all managed packages should be present. + // Two users each with two packages. + final int expectedUidSize = 2; + final List> uids = + new ArrayList<>(nris.get(0).mRequests.get(0).networkCapabilities.getUids()); + assertEquals(expectedUidSize, uids.size()); + + // Sort by uid to access nris by index + uids.sort(Comparator.comparingInt(uid -> uid.getLower())); + final int secondUserTestPackageUid = UserHandle.getUid(secondUser, TEST_PACKAGE_UID); + assertEquals(TEST_PACKAGE_UID, (int) uids.get(0).getLower()); + assertEquals(TEST_PACKAGE_UID, (int) uids.get(0).getUpper()); + assertEquals(secondUserTestPackageUid, (int) uids.get(1).getLower()); + assertEquals(secondUserTestPackageUid, (int) uids.get(1).getUpper()); + } + + @Test + public void testOemNetworkRequestFactoryAddsPackagesToCorrectPreference() + throws Exception { + // Expectations + final int expectedNumOfNris = 1; + final int expectedNumOfAppUids = 2; + + // Arrange PackageManager mocks + final String testPackageName2 = "com.google.apps.dialer"; + final int testPackageNameUid2 = 456; + mockGetApplicationInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_UID); + mockGetApplicationInfo(testPackageName2, testPackageNameUid2); + + // Build OemNetworkPreferences object + final int testOemPref = OEM_NETWORK_PREFERENCE_OEM_PAID; + final OemNetworkPreferences pref = new OemNetworkPreferences.Builder() + .addNetworkPreference(TEST_PACKAGE_NAME, testOemPref) + .addNetworkPreference(testPackageName2, testOemPref) + .build(); + + // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences() + final ArraySet nris = + mService.new OemNetworkRequestFactory().createNrisFromOemNetworkPreferences(pref); + + assertEquals(expectedNumOfNris, nris.size()); + assertEquals(expectedNumOfAppUids, + nris.iterator().next().mRequests.get(0).networkCapabilities.getUids().size()); + } + + @Test + public void testSetOemNetworkPreferenceNullListenerAndPrefParamThrowsNpe() { + mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, true); + + // Act on ConnectivityService.setOemNetworkPreference() + assertThrows(NullPointerException.class, + () -> mService.setOemNetworkPreference( + null, + null)); + } + + @Test + public void testSetOemNetworkPreferenceFailsForNonAutomotive() + throws Exception { + mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, false); + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY; + + // Act on ConnectivityService.setOemNetworkPreference() + assertThrows(UnsupportedOperationException.class, + () -> mService.setOemNetworkPreference( + createDefaultOemNetworkPreferences(networkPref), + new TestOemListenerCallback())); + } + + private void setOemNetworkPreferenceAgentConnected(final int transportType, + final boolean connectAgent) throws Exception { + switch(transportType) { + // Corresponds to a metered cellular network. Will be used for the default network. + case TRANSPORT_CELLULAR: + if (!connectAgent) { + mCellNetworkAgent.disconnect(); + break; + } + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED); + mCellNetworkAgent.connect(true); + break; + // Corresponds to a restricted ethernet network with OEM_PAID/OEM_PRIVATE. + case TRANSPORT_ETHERNET: + if (!connectAgent) { + stopOemManagedNetwork(); + break; + } + startOemManagedNetwork(true); + break; + // Corresponds to unmetered Wi-Fi. + case TRANSPORT_WIFI: + if (!connectAgent) { + mWiFiNetworkAgent.disconnect(); + break; + } + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.connect(true); + break; + default: + throw new AssertionError("Unsupported transport type passed in."); + + } + waitForIdle(); + } + + private void startOemManagedNetwork(final boolean isOemPaid) throws Exception { + mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET); + mEthernetNetworkAgent.addCapability( + isOemPaid ? NET_CAPABILITY_OEM_PAID : NET_CAPABILITY_OEM_PRIVATE); + mEthernetNetworkAgent.removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + mEthernetNetworkAgent.connect(true); + } + + private void stopOemManagedNetwork() { + mEthernetNetworkAgent.disconnect(); + waitForIdle(); + } + + private void verifyMultipleDefaultNetworksTracksCorrectly( + final int expectedOemRequestsSize, + @NonNull final Network expectedDefaultNetwork, + @NonNull final Network expectedPerAppNetwork) { + // The current test setup assumes two tracked default network requests; one for the default + // network and the other for the OEM network preference being tested. This will be validated + // each time to confirm it doesn't change under test. + final int expectedDefaultNetworkRequestsSize = 2; + assertEquals(expectedDefaultNetworkRequestsSize, mService.mDefaultNetworkRequests.size()); + for (final ConnectivityService.NetworkRequestInfo defaultRequest + : mService.mDefaultNetworkRequests) { + final Network defaultNetwork = defaultRequest.getSatisfier() == null + ? null : defaultRequest.getSatisfier().network(); + // If this is the default request. + if (defaultRequest == mService.mDefaultRequest) { + assertEquals( + expectedDefaultNetwork, + defaultNetwork); + // Make sure this value doesn't change. + assertEquals(1, defaultRequest.mRequests.size()); + continue; + } + assertEquals(expectedPerAppNetwork, defaultNetwork); + assertEquals(expectedOemRequestsSize, defaultRequest.mRequests.size()); + } + verifyMultipleDefaultCallbacks(expectedDefaultNetwork, expectedPerAppNetwork); + } + + /** + * Verify default callbacks for 'available' fire as expected. This will only run if + * registerDefaultNetworkCallbacks() was executed prior and will only be different if the + * setOemNetworkPreference() per-app API was used for the current process. + * @param expectedSystemDefault the expected network for the system default. + * @param expectedPerAppDefault the expected network for the current process's default. + */ + private void verifyMultipleDefaultCallbacks( + @NonNull final Network expectedSystemDefault, + @NonNull final Network expectedPerAppDefault) { + if (null != mSystemDefaultNetworkCallback && null != expectedSystemDefault + && mService.mNoServiceNetwork.network() != expectedSystemDefault) { + // getLastAvailableNetwork() is used as this method can be called successively with + // the same network to validate therefore expectAvailableThenValidatedCallbacks + // can't be used. + assertEquals(mSystemDefaultNetworkCallback.getLastAvailableNetwork(), + expectedSystemDefault); + } + if (null != mDefaultNetworkCallback && null != expectedPerAppDefault + && mService.mNoServiceNetwork.network() != expectedPerAppDefault) { + assertEquals(mDefaultNetworkCallback.getLastAvailableNetwork(), + expectedPerAppDefault); + } + } + + private void registerDefaultNetworkCallbacks() { + // Using Manifest.permission.NETWORK_SETTINGS for registerSystemDefaultNetworkCallback() + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + mSystemDefaultNetworkCallback = new TestNetworkCallback(); + mDefaultNetworkCallback = new TestNetworkCallback(); + mProfileDefaultNetworkCallback = new TestNetworkCallback(); + mCm.registerSystemDefaultNetworkCallback(mSystemDefaultNetworkCallback, + new Handler(ConnectivityThread.getInstanceLooper())); + mCm.registerDefaultNetworkCallback(mDefaultNetworkCallback); + registerDefaultNetworkCallbackAsUid(mProfileDefaultNetworkCallback, + TEST_WORK_PROFILE_APP_UID); + // TODO: test using ConnectivityManager#registerDefaultNetworkCallbackAsUid as well. + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_DENIED); + } + + private void unregisterDefaultNetworkCallbacks() { + if (null != mDefaultNetworkCallback) { + mCm.unregisterNetworkCallback(mDefaultNetworkCallback); + } + if (null != mSystemDefaultNetworkCallback) { + mCm.unregisterNetworkCallback(mSystemDefaultNetworkCallback); + } + if (null != mProfileDefaultNetworkCallback) { + mCm.unregisterNetworkCallback(mProfileDefaultNetworkCallback); + } + } + + private void setupMultipleDefaultNetworksForOemNetworkPreferenceNotCurrentUidTest( + @OemNetworkPreferences.OemNetworkPreference final int networkPrefToSetup) + throws Exception { + final int testPackageNameUid = TEST_PACKAGE_UID; + final String testPackageName = "per.app.defaults.package"; + setupMultipleDefaultNetworksForOemNetworkPreferenceTest( + networkPrefToSetup, testPackageNameUid, testPackageName); + } + + private void setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest( + @OemNetworkPreferences.OemNetworkPreference final int networkPrefToSetup) + throws Exception { + final int testPackageNameUid = Process.myUid(); + final String testPackageName = "per.app.defaults.package"; + setupMultipleDefaultNetworksForOemNetworkPreferenceTest( + networkPrefToSetup, testPackageNameUid, testPackageName); + } + + private void setupMultipleDefaultNetworksForOemNetworkPreferenceTest( + @OemNetworkPreferences.OemNetworkPreference final int networkPrefToSetup, + final int testPackageUid, @NonNull final String testPackageName) throws Exception { + // Only the default request should be included at start. + assertEquals(1, mService.mDefaultNetworkRequests.size()); + + final UidRangeParcel[] uidRanges = + toUidRangeStableParcels(uidRangesForUids(testPackageUid)); + setupSetOemNetworkPreferenceForPreferenceTest( + networkPrefToSetup, uidRanges, testPackageName); + } + + private void setupSetOemNetworkPreferenceForPreferenceTest( + @OemNetworkPreferences.OemNetworkPreference final int networkPrefToSetup, + @NonNull final UidRangeParcel[] uidRanges, + @NonNull final String testPackageName) + throws Exception { + // These tests work off a single UID therefore using 'start' is valid. + mockGetApplicationInfo(testPackageName, uidRanges[0].start); + + setOemNetworkPreference(networkPrefToSetup, testPackageName); + } + + private void setOemNetworkPreference(final int networkPrefToSetup, + @NonNull final String... testPackageNames) + throws Exception { + mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, true); + + // Build OemNetworkPreferences object + final OemNetworkPreferences.Builder builder = new OemNetworkPreferences.Builder(); + for (final String packageName : testPackageNames) { + builder.addNetworkPreference(packageName, networkPrefToSetup); + } + final OemNetworkPreferences pref = builder.build(); + + // Act on ConnectivityService.setOemNetworkPreference() + final TestOemListenerCallback oemPrefListener = new TestOemListenerCallback(); + mService.setOemNetworkPreference(pref, oemPrefListener); + + // Verify call returned successfully + oemPrefListener.expectOnComplete(); + } + + private static class TestOemListenerCallback implements IOnCompleteListener { + final CompletableFuture mDone = new CompletableFuture<>(); + + @Override + public void onComplete() { + mDone.complete(new Object()); + } + + void expectOnComplete() { + try { + mDone.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + fail("Expected onComplete() not received after " + TIMEOUT_MS + " ms"); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Override + public IBinder asBinder() { + return null; + } + } + + @Test + public void testMultiDefaultGetActiveNetworkIsCorrect() throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY; + final int expectedOemPrefRequestSize = 1; + registerDefaultNetworkCallbacks(); + + // Setup the test process to use networkPref for their default network. + setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref); + + // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID. + // The active network for the default should be null at this point as this is a retricted + // network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + mEthernetNetworkAgent.getNetwork()); + + // Verify that the active network is correct + verifyActiveNetwork(TRANSPORT_ETHERNET); + // default NCs will be unregistered in tearDown + } + + @Test + public void testMultiDefaultIsActiveNetworkMeteredIsCorrect() throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY; + final int expectedOemPrefRequestSize = 1; + registerDefaultNetworkCallbacks(); + + // Setup the test process to use networkPref for their default network. + setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref); + + // Returns true by default when no network is available. + assertTrue(mCm.isActiveNetworkMetered()); + + // Connect to an unmetered restricted network that will only be available to the OEM pref. + mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET); + mEthernetNetworkAgent.addCapability(NET_CAPABILITY_OEM_PAID); + mEthernetNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mEthernetNetworkAgent.removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + mEthernetNetworkAgent.connect(true); + waitForIdle(); + + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + mEthernetNetworkAgent.getNetwork()); + + assertFalse(mCm.isActiveNetworkMetered()); + // default NCs will be unregistered in tearDown + } + + @Test + public void testPerAppDefaultRegisterDefaultNetworkCallback() throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY; + final int expectedOemPrefRequestSize = 1; + final TestNetworkCallback defaultNetworkCallback = new TestNetworkCallback(); + + // Register the default network callback before the pref is already set. This means that + // the policy will be applied to the callback on setOemNetworkPreference(). + mCm.registerDefaultNetworkCallback(defaultNetworkCallback); + defaultNetworkCallback.assertNoCallback(); + + final TestNetworkCallback otherUidDefaultCallback = new TestNetworkCallback(); + withPermission(NETWORK_SETTINGS, () -> + mCm.registerDefaultNetworkCallbackForUid(TEST_PACKAGE_UID, otherUidDefaultCallback, + new Handler(ConnectivityThread.getInstanceLooper()))); + + // Setup the test process to use networkPref for their default network. + setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref); + + // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID. + // The active nai for the default is null at this point as this is a restricted network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + mEthernetNetworkAgent.getNetwork()); + + // At this point with a restricted network used, the available callback should trigger. + defaultNetworkCallback.expectAvailableThenValidatedCallbacks(mEthernetNetworkAgent); + assertEquals(defaultNetworkCallback.getLastAvailableNetwork(), + mEthernetNetworkAgent.getNetwork()); + otherUidDefaultCallback.assertNoCallback(); + + // Now bring down the default network which should trigger a LOST callback. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false); + + // At this point, with no network is available, the lost callback should trigger + defaultNetworkCallback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent); + otherUidDefaultCallback.assertNoCallback(); + + // Confirm we can unregister without issues. + mCm.unregisterNetworkCallback(defaultNetworkCallback); + mCm.unregisterNetworkCallback(otherUidDefaultCallback); + } + + @Test + public void testPerAppDefaultRegisterDefaultNetworkCallbackAfterPrefSet() throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY; + final int expectedOemPrefRequestSize = 1; + final TestNetworkCallback defaultNetworkCallback = new TestNetworkCallback(); + + // Setup the test process to use networkPref for their default network. + setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref); + + // Register the default network callback after the pref is already set. This means that + // the policy will be applied to the callback on requestNetwork(). + mCm.registerDefaultNetworkCallback(defaultNetworkCallback); + defaultNetworkCallback.assertNoCallback(); + + final TestNetworkCallback otherUidDefaultCallback = new TestNetworkCallback(); + withPermission(NETWORK_SETTINGS, () -> + mCm.registerDefaultNetworkCallbackForUid(TEST_PACKAGE_UID, otherUidDefaultCallback, + new Handler(ConnectivityThread.getInstanceLooper()))); + + // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID. + // The active nai for the default is null at this point as this is a restricted network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + mEthernetNetworkAgent.getNetwork()); + + // At this point with a restricted network used, the available callback should trigger + defaultNetworkCallback.expectAvailableThenValidatedCallbacks(mEthernetNetworkAgent); + assertEquals(defaultNetworkCallback.getLastAvailableNetwork(), + mEthernetNetworkAgent.getNetwork()); + otherUidDefaultCallback.assertNoCallback(); + + // Now bring down the default network which should trigger a LOST callback. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false); + otherUidDefaultCallback.assertNoCallback(); + + // At this point, with no network is available, the lost callback should trigger + defaultNetworkCallback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent); + otherUidDefaultCallback.assertNoCallback(); + + // Confirm we can unregister without issues. + mCm.unregisterNetworkCallback(defaultNetworkCallback); + mCm.unregisterNetworkCallback(otherUidDefaultCallback); + } + + @Test + public void testPerAppDefaultRegisterDefaultNetworkCallbackDoesNotFire() throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY; + final int expectedOemPrefRequestSize = 1; + final TestNetworkCallback defaultNetworkCallback = new TestNetworkCallback(); + final int userId = UserHandle.getUserId(Process.myUid()); + + mCm.registerDefaultNetworkCallback(defaultNetworkCallback); + defaultNetworkCallback.assertNoCallback(); + + final TestNetworkCallback otherUidDefaultCallback = new TestNetworkCallback(); + withPermission(NETWORK_SETTINGS, () -> + mCm.registerDefaultNetworkCallbackForUid(TEST_PACKAGE_UID, otherUidDefaultCallback, + new Handler(ConnectivityThread.getInstanceLooper()))); + + // Setup a process different than the test process to use the default network. This means + // that the defaultNetworkCallback won't be tracked by the per-app policy. + setupMultipleDefaultNetworksForOemNetworkPreferenceNotCurrentUidTest(networkPref); + + // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID. + // The active nai for the default is null at this point as this is a restricted network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + mEthernetNetworkAgent.getNetwork()); + + // As this callback does not have access to the OEM_PAID network, it will not fire. + defaultNetworkCallback.assertNoCallback(); + assertDefaultNetworkCapabilities(userId /* no networks */); + + // The other UID does have access, and gets a callback. + otherUidDefaultCallback.expectAvailableThenValidatedCallbacks(mEthernetNetworkAgent); + + // Bring up unrestricted cellular. This should now satisfy the default network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // At this point with an unrestricted network used, the available callback should trigger + // The other UID is unaffected and remains on the paid network. + defaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + assertEquals(defaultNetworkCallback.getLastAvailableNetwork(), + mCellNetworkAgent.getNetwork()); + assertDefaultNetworkCapabilities(userId, mCellNetworkAgent); + otherUidDefaultCallback.assertNoCallback(); + + // Now bring down the per-app network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false); + + // Since the callback didn't use the per-app network, only the other UID gets a callback. + // Because the preference specifies no fallback, it does not switch to cellular. + defaultNetworkCallback.assertNoCallback(); + otherUidDefaultCallback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent); + + // Now bring down the default network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, false); + + // As this callback was tracking the default, this should now trigger. + defaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + otherUidDefaultCallback.assertNoCallback(); + + // Confirm we can unregister without issues. + mCm.unregisterNetworkCallback(defaultNetworkCallback); + mCm.unregisterNetworkCallback(otherUidDefaultCallback); + } + + /** + * This method assumes that the same uidRanges input will be used to verify that dependencies + * are called as expected. + */ + private void verifySetOemNetworkPreferenceForPreference( + @NonNull final UidRangeParcel[] uidRanges, + final int addUidRangesNetId, + final int addUidRangesTimes, + final int removeUidRangesNetId, + final int removeUidRangesTimes, + final boolean shouldDestroyNetwork) throws RemoteException { + verifySetOemNetworkPreferenceForPreference(uidRanges, uidRanges, + addUidRangesNetId, addUidRangesTimes, removeUidRangesNetId, removeUidRangesTimes, + shouldDestroyNetwork); + } + + private void verifySetOemNetworkPreferenceForPreference( + @NonNull final UidRangeParcel[] addedUidRanges, + @NonNull final UidRangeParcel[] removedUidRanges, + final int addUidRangesNetId, + final int addUidRangesTimes, + final int removeUidRangesNetId, + final int removeUidRangesTimes, + final boolean shouldDestroyNetwork) throws RemoteException { + final boolean useAnyIdForAdd = OEM_PREF_ANY_NET_ID == addUidRangesNetId; + final boolean useAnyIdForRemove = OEM_PREF_ANY_NET_ID == removeUidRangesNetId; + + // Validate netd. + verify(mMockNetd, times(addUidRangesTimes)) + .networkAddUidRanges( + (useAnyIdForAdd ? anyInt() : eq(addUidRangesNetId)), eq(addedUidRanges)); + verify(mMockNetd, times(removeUidRangesTimes)) + .networkRemoveUidRanges( + (useAnyIdForRemove ? anyInt() : eq(removeUidRangesNetId)), + eq(removedUidRanges)); + if (shouldDestroyNetwork) { + verify(mMockNetd, times(1)) + .networkDestroy((useAnyIdForRemove ? anyInt() : eq(removeUidRangesNetId))); + } + reset(mMockNetd); + } + + /** + * Test the tracked default requests clear previous OEM requests on setOemNetworkPreference(). + */ + @Test + public void testSetOemNetworkPreferenceClearPreviousOemValues() throws Exception { + @OemNetworkPreferences.OemNetworkPreference int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID; + final int testPackageUid = 123; + final String testPackageName = "com.google.apps.contacts"; + final UidRangeParcel[] uidRanges = + toUidRangeStableParcels(uidRangesForUids(testPackageUid)); + + // Validate the starting requests only includes the fallback request. + assertEquals(1, mService.mDefaultNetworkRequests.size()); + + // Add an OEM default network request to track. + setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, testPackageName); + + // Two requests should exist, one for the fallback and one for the pref. + assertEquals(2, mService.mDefaultNetworkRequests.size()); + + networkPref = OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY; + setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, testPackageName); + + // Two requests should still exist validating the previous per-app request was replaced. + assertEquals(2, mService.mDefaultNetworkRequests.size()); + } + + /** + * Test network priority for preference OEM_NETWORK_PREFERENCE_OEM_PAID in the following order: + * NET_CAPABILITY_NOT_METERED -> NET_CAPABILITY_OEM_PAID -> fallback + */ + @Test + public void testMultilayerForPreferenceOemPaidEvaluatesCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID; + + // Arrange PackageManager mocks + final UidRangeParcel[] uidRanges = + toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID)); + setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME); + + // Verify the starting state. No networks should be connected. + verifySetOemNetworkPreferenceForPreference(uidRanges, + OEM_PREF_ANY_NET_ID, 0 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Test lowest to highest priority requests. + // Bring up metered cellular. This will satisfy the fallback network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mEthernetNetworkAgent.getNetwork().netId, 1 /* times */, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + false /* shouldDestroyNetwork */); + + // Bring up unmetered Wi-Fi. This will satisfy NET_CAPABILITY_NOT_METERED. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mWiFiNetworkAgent.getNetwork().netId, 1 /* times */, + mEthernetNetworkAgent.getNetwork().netId, 1 /* times */, + false /* shouldDestroyNetwork */); + + // Disconnecting OEM_PAID should have no effect as it is lower in priority then unmetered. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false); + // netd should not be called as default networks haven't changed. + verifySetOemNetworkPreferenceForPreference(uidRanges, + OEM_PREF_ANY_NET_ID, 0 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Disconnecting unmetered should put PANS on lowest priority fallback request. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, false); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + mWiFiNetworkAgent.getNetwork().netId, 0 /* times */, + true /* shouldDestroyNetwork */); + + // Disconnecting the fallback network should result in no connectivity. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, false); + verifySetOemNetworkPreferenceForPreference(uidRanges, + OEM_PREF_ANY_NET_ID, 0 /* times */, + mCellNetworkAgent.getNetwork().netId, 0 /* times */, + true /* shouldDestroyNetwork */); + } + + /** + * Test network priority for OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK in the following order: + * NET_CAPABILITY_NOT_METERED -> NET_CAPABILITY_OEM_PAID + */ + @Test + public void testMultilayerForPreferenceOemPaidNoFallbackEvaluatesCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK; + + // Arrange PackageManager mocks + final UidRangeParcel[] uidRanges = + toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID)); + setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME); + + // Verify the starting state. This preference doesn't support using the fallback network + // therefore should be on the disconnected network as it has no networks to connect to. + verifySetOemNetworkPreferenceForPreference(uidRanges, + mService.mNoServiceNetwork.network.getNetId(), 1 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Test lowest to highest priority requests. + // Bring up metered cellular. This will satisfy the fallback network. + // This preference should not use this network as it doesn't support fallback usage. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + OEM_PREF_ANY_NET_ID, 0 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mEthernetNetworkAgent.getNetwork().netId, 1 /* times */, + mService.mNoServiceNetwork.network.getNetId(), 1 /* times */, + false /* shouldDestroyNetwork */); + + // Bring up unmetered Wi-Fi. This will satisfy NET_CAPABILITY_NOT_METERED. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mWiFiNetworkAgent.getNetwork().netId, 1 /* times */, + mEthernetNetworkAgent.getNetwork().netId, 1 /* times */, + false /* shouldDestroyNetwork */); + + // Disconnecting unmetered should put PANS on OEM_PAID. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, false); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mEthernetNetworkAgent.getNetwork().netId, 1 /* times */, + mWiFiNetworkAgent.getNetwork().netId, 0 /* times */, + true /* shouldDestroyNetwork */); + + // Disconnecting OEM_PAID should result in no connectivity. + // OEM_PAID_NO_FALLBACK not supporting a fallback now uses the disconnected network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mService.mNoServiceNetwork.network.getNetId(), 1 /* times */, + mEthernetNetworkAgent.getNetwork().netId, 0 /* times */, + true /* shouldDestroyNetwork */); + } + + /** + * Test network priority for OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY in the following order: + * NET_CAPABILITY_OEM_PAID + * This preference should only apply to OEM_PAID networks. + */ + @Test + public void testMultilayerForPreferenceOemPaidOnlyEvaluatesCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY; + + // Arrange PackageManager mocks + final UidRangeParcel[] uidRanges = + toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID)); + setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME); + + // Verify the starting state. This preference doesn't support using the fallback network + // therefore should be on the disconnected network as it has no networks to connect to. + verifySetOemNetworkPreferenceForPreference(uidRanges, + mService.mNoServiceNetwork.network.getNetId(), 1 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Bring up metered cellular. This should not apply to this preference. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + OEM_PREF_ANY_NET_ID, 0 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Bring up unmetered Wi-Fi. This should not apply to this preference. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + OEM_PREF_ANY_NET_ID, 0 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mEthernetNetworkAgent.getNetwork().netId, 1 /* times */, + mService.mNoServiceNetwork.network.getNetId(), 1 /* times */, + false /* shouldDestroyNetwork */); + + // Disconnecting OEM_PAID should result in no connectivity. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mService.mNoServiceNetwork.network.getNetId(), 1 /* times */, + mEthernetNetworkAgent.getNetwork().netId, 0 /* times */, + true /* shouldDestroyNetwork */); + } + + /** + * Test network priority for OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY in the following order: + * NET_CAPABILITY_OEM_PRIVATE + * This preference should only apply to OEM_PRIVATE networks. + */ + @Test + public void testMultilayerForPreferenceOemPrivateOnlyEvaluatesCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY; + + // Arrange PackageManager mocks + final UidRangeParcel[] uidRanges = + toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID)); + setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME); + + // Verify the starting state. This preference doesn't support using the fallback network + // therefore should be on the disconnected network as it has no networks to connect to. + verifySetOemNetworkPreferenceForPreference(uidRanges, + mService.mNoServiceNetwork.network.getNetId(), 1 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Bring up metered cellular. This should not apply to this preference. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + OEM_PREF_ANY_NET_ID, 0 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Bring up unmetered Wi-Fi. This should not apply to this preference. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + OEM_PREF_ANY_NET_ID, 0 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Bring up ethernet with OEM_PRIVATE. This will satisfy NET_CAPABILITY_OEM_PRIVATE. + startOemManagedNetwork(false); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mEthernetNetworkAgent.getNetwork().netId, 1 /* times */, + mService.mNoServiceNetwork.network.getNetId(), 1 /* times */, + false /* shouldDestroyNetwork */); + + // Disconnecting OEM_PRIVATE should result in no connectivity. + stopOemManagedNetwork(); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mService.mNoServiceNetwork.network.getNetId(), 1 /* times */, + mEthernetNetworkAgent.getNetwork().netId, 0 /* times */, + true /* shouldDestroyNetwork */); + } + + @Test + public void testMultilayerForMultipleUsersEvaluatesCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID; + + // Arrange users + final int secondUser = 10; + final UserHandle secondUserHandle = new UserHandle(secondUser); + when(mUserManager.getUserHandles(anyBoolean())).thenReturn( + Arrays.asList(PRIMARY_USER_HANDLE, secondUserHandle)); + + // Arrange PackageManager mocks + final int secondUserTestPackageUid = UserHandle.getUid(secondUser, TEST_PACKAGE_UID); + final UidRangeParcel[] uidRanges = + toUidRangeStableParcels( + uidRangesForUids(TEST_PACKAGE_UID, secondUserTestPackageUid)); + setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME); + + // Verify the starting state. No networks should be connected. + verifySetOemNetworkPreferenceForPreference(uidRanges, + OEM_PREF_ANY_NET_ID, 0 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Test that we correctly add the expected values for multiple users. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Test that we correctly remove the expected values for multiple users. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, false); + verifySetOemNetworkPreferenceForPreference(uidRanges, + OEM_PREF_ANY_NET_ID, 0 /* times */, + mCellNetworkAgent.getNetwork().netId, 0 /* times */, + true /* shouldDestroyNetwork */); + } + + @Test + public void testMultilayerForBroadcastedUsersEvaluatesCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID; + + // Arrange users + final int secondUser = 10; + final UserHandle secondUserHandle = new UserHandle(secondUser); + when(mUserManager.getUserHandles(anyBoolean())).thenReturn( + Arrays.asList(PRIMARY_USER_HANDLE)); + + // Arrange PackageManager mocks + final int secondUserTestPackageUid = UserHandle.getUid(secondUser, TEST_PACKAGE_UID); + final UidRangeParcel[] uidRangesSingleUser = + toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID)); + final UidRangeParcel[] uidRangesBothUsers = + toUidRangeStableParcels( + uidRangesForUids(TEST_PACKAGE_UID, secondUserTestPackageUid)); + setupSetOemNetworkPreferenceForPreferenceTest( + networkPref, uidRangesSingleUser, TEST_PACKAGE_NAME); + + // Verify the starting state. No networks should be connected. + verifySetOemNetworkPreferenceForPreference(uidRangesSingleUser, + OEM_PREF_ANY_NET_ID, 0 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Test that we correctly add the expected values for multiple users. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifySetOemNetworkPreferenceForPreference(uidRangesSingleUser, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Send a broadcast indicating a user was added. + when(mUserManager.getUserHandles(anyBoolean())).thenReturn( + Arrays.asList(PRIMARY_USER_HANDLE, secondUserHandle)); + final Intent addedIntent = new Intent(ACTION_USER_ADDED); + addedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(secondUser)); + processBroadcast(addedIntent); + + // Test that we correctly add values for all users and remove for the single user. + verifySetOemNetworkPreferenceForPreference(uidRangesBothUsers, uidRangesSingleUser, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + false /* shouldDestroyNetwork */); + + // Send a broadcast indicating a user was removed. + when(mUserManager.getUserHandles(anyBoolean())).thenReturn( + Arrays.asList(PRIMARY_USER_HANDLE)); + final Intent removedIntent = new Intent(ACTION_USER_REMOVED); + removedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(secondUser)); + processBroadcast(removedIntent); + + // Test that we correctly add values for the single user and remove for the all users. + verifySetOemNetworkPreferenceForPreference(uidRangesSingleUser, uidRangesBothUsers, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + false /* shouldDestroyNetwork */); + } + + @Test + public void testMultilayerForPackageChangesEvaluatesCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID; + final String packageScheme = "package:"; + + // Arrange PackageManager mocks + final String packageToInstall = "package.to.install"; + final int packageToInstallUid = 81387; + final UidRangeParcel[] uidRangesSinglePackage = + toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID)); + mockGetApplicationInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_UID); + mockGetApplicationInfoThrowsNameNotFound(packageToInstall); + setOemNetworkPreference(networkPref, TEST_PACKAGE_NAME, packageToInstall); + grantUsingBackgroundNetworksPermissionForUid(Binder.getCallingUid(), packageToInstall); + + // Verify the starting state. No networks should be connected. + verifySetOemNetworkPreferenceForPreference(uidRangesSinglePackage, + OEM_PREF_ANY_NET_ID, 0 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Test that we correctly add the expected values for installed packages. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifySetOemNetworkPreferenceForPreference(uidRangesSinglePackage, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Set the system to recognize the package to be installed + mockGetApplicationInfo(packageToInstall, packageToInstallUid); + final UidRangeParcel[] uidRangesAllPackages = + toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID, packageToInstallUid)); + + // Send a broadcast indicating a package was installed. + final Intent addedIntent = new Intent(ACTION_PACKAGE_ADDED); + addedIntent.setData(Uri.parse(packageScheme + packageToInstall)); + processBroadcast(addedIntent); + + // Test the single package is removed and the combined packages are added. + verifySetOemNetworkPreferenceForPreference(uidRangesAllPackages, uidRangesSinglePackage, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + false /* shouldDestroyNetwork */); + + // Set the system to no longer recognize the package to be installed + mockGetApplicationInfoThrowsNameNotFound(packageToInstall); + + // Send a broadcast indicating a package was removed. + final Intent removedIntent = new Intent(ACTION_PACKAGE_REMOVED); + removedIntent.setData(Uri.parse(packageScheme + packageToInstall)); + processBroadcast(removedIntent); + + // Test the combined packages are removed and the single package is added. + verifySetOemNetworkPreferenceForPreference(uidRangesSinglePackage, uidRangesAllPackages, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + false /* shouldDestroyNetwork */); + + // Set the system to change the installed package's uid + final int replacedTestPackageUid = TEST_PACKAGE_UID + 1; + mockGetApplicationInfo(TEST_PACKAGE_NAME, replacedTestPackageUid); + final UidRangeParcel[] uidRangesReplacedPackage = + toUidRangeStableParcels(uidRangesForUids(replacedTestPackageUid)); + + // Send a broadcast indicating a package was replaced. + final Intent replacedIntent = new Intent(ACTION_PACKAGE_REPLACED); + replacedIntent.setData(Uri.parse(packageScheme + TEST_PACKAGE_NAME)); + processBroadcast(replacedIntent); + + // Test the original uid is removed and is replaced with the new uid. + verifySetOemNetworkPreferenceForPreference(uidRangesReplacedPackage, uidRangesSinglePackage, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + false /* shouldDestroyNetwork */); + } + + /** + * Test network priority for preference OEM_NETWORK_PREFERENCE_OEM_PAID in the following order: + * NET_CAPABILITY_NOT_METERED -> NET_CAPABILITY_OEM_PAID -> fallback + */ + @Test + public void testMultipleDefaultNetworksTracksOemNetworkPreferenceOemPaidCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID; + setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref); + final int expectedDefaultRequestSize = 2; + final int expectedOemPrefRequestSize = 3; + registerDefaultNetworkCallbacks(); + + // The fallback as well as the OEM preference should now be tracked. + assertEquals(expectedDefaultRequestSize, mService.mDefaultNetworkRequests.size()); + + // Test lowest to highest priority requests. + // Bring up metered cellular. This will satisfy the fallback network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mCellNetworkAgent.getNetwork()); + + // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // Bring up unmetered Wi-Fi. This will satisfy NET_CAPABILITY_NOT_METERED. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mWiFiNetworkAgent.getNetwork(), + mWiFiNetworkAgent.getNetwork()); + + // Disconnecting unmetered Wi-Fi will put the pref on OEM_PAID and fallback on cellular. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // Disconnecting cellular should keep OEM network on OEM_PAID and fallback will be null. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + mEthernetNetworkAgent.getNetwork()); + + // Disconnecting OEM_PAID will put both on null as it is the last network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + null); + + // default callbacks will be unregistered in tearDown + } + + /** + * Test network priority for OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK in the following order: + * NET_CAPABILITY_NOT_METERED -> NET_CAPABILITY_OEM_PAID + */ + @Test + public void testMultipleDefaultNetworksTracksOemNetworkPreferenceOemPaidNoFallbackCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK; + setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref); + final int expectedDefaultRequestSize = 2; + final int expectedOemPrefRequestSize = 2; + registerDefaultNetworkCallbacks(); + + // The fallback as well as the OEM preference should now be tracked. + assertEquals(expectedDefaultRequestSize, mService.mDefaultNetworkRequests.size()); + + // Test lowest to highest priority requests. + // Bring up metered cellular. This will satisfy the fallback network but not the pref. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mService.mNoServiceNetwork.network()); + + // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // Bring up unmetered Wi-Fi. This will satisfy NET_CAPABILITY_NOT_METERED. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mWiFiNetworkAgent.getNetwork(), + mWiFiNetworkAgent.getNetwork()); + + // Disconnecting unmetered Wi-Fi will put the OEM pref on OEM_PAID and fallback on cellular. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // Disconnecting cellular should keep OEM network on OEM_PAID and fallback will be null. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + mEthernetNetworkAgent.getNetwork()); + + // Disconnecting OEM_PAID puts the fallback on null and the pref on the disconnected net. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + mService.mNoServiceNetwork.network()); + + // default callbacks will be unregistered in tearDown + } + + /** + * Test network priority for OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY in the following order: + * NET_CAPABILITY_OEM_PAID + * This preference should only apply to OEM_PAID networks. + */ + @Test + public void testMultipleDefaultNetworksTracksOemNetworkPreferenceOemPaidOnlyCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY; + setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref); + final int expectedDefaultRequestSize = 2; + final int expectedOemPrefRequestSize = 1; + registerDefaultNetworkCallbacks(); + + // The fallback as well as the OEM preference should now be tracked. + assertEquals(expectedDefaultRequestSize, mService.mDefaultNetworkRequests.size()); + + // Test lowest to highest priority requests. + // Bring up metered cellular. This will satisfy the fallback network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mService.mNoServiceNetwork.network()); + + // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // Bring up unmetered Wi-Fi. The OEM network shouldn't change, the fallback will take Wi-Fi. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mWiFiNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // Disconnecting unmetered Wi-Fi shouldn't change the OEM network with fallback on cellular. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // Disconnecting OEM_PAID will keep the fallback on cellular and nothing for OEM_PAID. + // OEM_PAID_ONLY not supporting a fallback now uses the disconnected network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mService.mNoServiceNetwork.network()); + + // Disconnecting cellular will put the fallback on null and the pref on disconnected. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + mService.mNoServiceNetwork.network()); + + // default callbacks will be unregistered in tearDown + } + + /** + * Test network priority for OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY in the following order: + * NET_CAPABILITY_OEM_PRIVATE + * This preference should only apply to OEM_PRIVATE networks. + */ + @Test + public void testMultipleDefaultNetworksTracksOemNetworkPreferenceOemPrivateOnlyCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY; + setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref); + final int expectedDefaultRequestSize = 2; + final int expectedOemPrefRequestSize = 1; + registerDefaultNetworkCallbacks(); + + // The fallback as well as the OEM preference should now be tracked. + assertEquals(expectedDefaultRequestSize, mService.mDefaultNetworkRequests.size()); + + // Test lowest to highest priority requests. + // Bring up metered cellular. This will satisfy the fallback network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mService.mNoServiceNetwork.network()); + + // Bring up ethernet with OEM_PRIVATE. This will satisfy NET_CAPABILITY_OEM_PRIVATE. + startOemManagedNetwork(false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // Bring up unmetered Wi-Fi. The OEM network shouldn't change, the fallback will take Wi-Fi. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mWiFiNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // Disconnecting unmetered Wi-Fi shouldn't change the OEM network with fallback on cellular. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // Disconnecting OEM_PRIVATE will keep the fallback on cellular. + // OEM_PRIVATE_ONLY not supporting a fallback now uses to the disconnected network. + stopOemManagedNetwork(); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mService.mNoServiceNetwork.network()); + + // Disconnecting cellular will put the fallback on null and pref on disconnected. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + mService.mNoServiceNetwork.network()); + + // default callbacks will be unregistered in tearDown + } + + @Test + public void testCapabilityWithOemNetworkPreference() throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY; + setupMultipleDefaultNetworksForOemNetworkPreferenceNotCurrentUidTest(networkPref); + registerDefaultNetworkCallbacks(); + + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + + mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + + mCellNetworkAgent.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED); + mSystemDefaultNetworkCallback.expectCapabilitiesThat(mCellNetworkAgent, nc -> + nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)); + mDefaultNetworkCallback.expectCapabilitiesThat(mCellNetworkAgent, nc -> + nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)); + + // default callbacks will be unregistered in tearDown + } + + @Test + public void testSetOemNetworkPreferenceLogsRequest() throws Exception { + mServiceContext.setPermission(DUMP, PERMISSION_GRANTED); + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID; + final StringWriter stringWriter = new StringWriter(); + final String logIdentifier = "UPDATE INITIATED: OemNetworkPreferences"; + final Pattern pattern = Pattern.compile(logIdentifier); + + final int expectedNumLogs = 2; + final UidRangeParcel[] uidRanges = + toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID)); + + // Call twice to generate two logs. + setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME); + setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME); + mService.dump(new FileDescriptor(), new PrintWriter(stringWriter), new String[0]); + + final String dumpOutput = stringWriter.toString(); + final Matcher matcher = pattern.matcher(dumpOutput); + int count = 0; + while (matcher.find()) { + count++; + } + assertEquals(expectedNumLogs, count); + } + + @Test + public void testGetAllNetworkStateSnapshots() throws Exception { + verifyNoNetwork(); + + // Setup test cellular network with specified LinkProperties and NetworkCapabilities, + // verify the content of the snapshot matches. + final LinkProperties cellLp = new LinkProperties(); + final LinkAddress myIpv4Addr = new LinkAddress(InetAddress.getByName("192.0.2.129"), 25); + final LinkAddress myIpv6Addr = new LinkAddress(InetAddress.getByName("2001:db8::1"), 64); + cellLp.setInterfaceName("test01"); + cellLp.addLinkAddress(myIpv4Addr); + cellLp.addLinkAddress(myIpv6Addr); + cellLp.addRoute(new RouteInfo(InetAddress.getByName("fe80::1234"))); + cellLp.addRoute(new RouteInfo(InetAddress.getByName("192.0.2.254"))); + cellLp.addRoute(new RouteInfo(myIpv4Addr, null)); + cellLp.addRoute(new RouteInfo(myIpv6Addr, null)); + final NetworkCapabilities cellNcTemplate = new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_CELLULAR).addCapability(NET_CAPABILITY_MMS).build(); + + final TestNetworkCallback cellCb = new TestNetworkCallback(); + mCm.requestNetwork(new NetworkRequest.Builder().addCapability(NET_CAPABILITY_MMS).build(), + cellCb); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp, cellNcTemplate); + mCellNetworkAgent.connect(true); + cellCb.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + List snapshots = mCm.getAllNetworkStateSnapshots(); + assertLength(1, snapshots); + + // Compose the expected cellular snapshot for verification. + final NetworkCapabilities cellNc = + mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()); + final NetworkStateSnapshot cellSnapshot = new NetworkStateSnapshot( + mCellNetworkAgent.getNetwork(), cellNc, cellLp, + null, ConnectivityManager.TYPE_MOBILE); + assertEquals(cellSnapshot, snapshots.get(0)); + + // Connect wifi and verify the snapshots. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + waitForIdle(); + // Compose the expected wifi snapshot for verification. + final NetworkCapabilities wifiNc = + mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()); + final NetworkStateSnapshot wifiSnapshot = new NetworkStateSnapshot( + mWiFiNetworkAgent.getNetwork(), wifiNc, new LinkProperties(), null, + ConnectivityManager.TYPE_WIFI); + + snapshots = mCm.getAllNetworkStateSnapshots(); + assertLength(2, snapshots); + assertContainsAll(snapshots, cellSnapshot, wifiSnapshot); + + // Set cellular as suspended, verify the snapshots will not contain suspended networks. + // TODO: Consider include SUSPENDED networks, which should be considered as + // temporary shortage of connectivity of a connected network. + mCellNetworkAgent.suspend(); + waitForIdle(); + snapshots = mCm.getAllNetworkStateSnapshots(); + assertLength(1, snapshots); + assertEquals(wifiSnapshot, snapshots.get(0)); + + // Disconnect wifi, verify the snapshots contain nothing. + mWiFiNetworkAgent.disconnect(); + waitForIdle(); + snapshots = mCm.getAllNetworkStateSnapshots(); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertLength(0, snapshots); + + mCellNetworkAgent.resume(); + waitForIdle(); + snapshots = mCm.getAllNetworkStateSnapshots(); + assertLength(1, snapshots); + assertEquals(cellSnapshot, snapshots.get(0)); + + mCellNetworkAgent.disconnect(); + waitForIdle(); + verifyNoNetwork(); + mCm.unregisterNetworkCallback(cellCb); + } + + // Cannot be part of MockNetworkFactory since it requires method of the test. + private void expectNoRequestChanged(@NonNull MockNetworkFactory factory) { + waitForIdle(); + factory.assertNoRequestChanged(); + } + + @Test + public void testRegisterBestMatchingNetworkCallback_noIssueToFactory() throws Exception { + // Prepare mock mms factory. + final HandlerThread handlerThread = new HandlerThread("MockCellularFactory"); + handlerThread.start(); + NetworkCapabilities filter = new NetworkCapabilities() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_MMS); + final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(), + mServiceContext, "testFactory", filter, mCsHandlerThread); + testFactory.setScoreFilter(40); + + try { + // Register the factory and expect it will see default request, because all requests + // are sent to all factories. + testFactory.register(); + testFactory.expectRequestAdd(); + testFactory.assertRequestCountEquals(1); + // The factory won't try to start the network since the default request doesn't + // match the filter (no INTERNET capability). + assertFalse(testFactory.getMyStartRequested()); + + // Register callback for listening best matching network. Verify that the request won't + // be sent to factory. + final TestNetworkCallback bestMatchingCb = new TestNetworkCallback(); + mCm.registerBestMatchingNetworkCallback( + new NetworkRequest.Builder().addCapability(NET_CAPABILITY_MMS).build(), + bestMatchingCb, mCsHandlerThread.getThreadHandler()); + bestMatchingCb.assertNoCallback(); + expectNoRequestChanged(testFactory); + testFactory.assertRequestCountEquals(1); + assertFalse(testFactory.getMyStartRequested()); + + // Fire a normal mms request, verify the factory will only see the request. + final TestNetworkCallback mmsNetworkCallback = new TestNetworkCallback(); + final NetworkRequest mmsRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_MMS).build(); + mCm.requestNetwork(mmsRequest, mmsNetworkCallback); + testFactory.expectRequestAdd(); + testFactory.assertRequestCountEquals(2); + assertTrue(testFactory.getMyStartRequested()); + + // Unregister best matching callback, verify factory see no change. + mCm.unregisterNetworkCallback(bestMatchingCb); + expectNoRequestChanged(testFactory); + testFactory.assertRequestCountEquals(2); + assertTrue(testFactory.getMyStartRequested()); + } finally { + testFactory.terminate(); + } + } + + @Test + public void testRegisterBestMatchingNetworkCallback_trackBestNetwork() throws Exception { + final TestNetworkCallback bestMatchingCb = new TestNetworkCallback(); + mCm.registerBestMatchingNetworkCallback( + new NetworkRequest.Builder().addCapability(NET_CAPABILITY_TRUSTED).build(), + bestMatchingCb, mCsHandlerThread.getThreadHandler()); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + bestMatchingCb.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + bestMatchingCb.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + + // Change something on cellular to trigger capabilities changed, since the callback + // only cares about the best network, verify it received nothing from cellular. + mCellNetworkAgent.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED); + bestMatchingCb.assertNoCallback(); + + // Make cellular the best network again, verify the callback now tracks cellular. + mWiFiNetworkAgent.adjustScore(-50); + bestMatchingCb.expectAvailableCallbacksValidated(mCellNetworkAgent); + + // Make cellular temporary non-trusted, which will not satisfying the request. + // Verify the callback switch from/to the other network accordingly. + mCellNetworkAgent.removeCapability(NET_CAPABILITY_TRUSTED); + bestMatchingCb.expectAvailableCallbacksValidated(mWiFiNetworkAgent); + mCellNetworkAgent.addCapability(NET_CAPABILITY_TRUSTED); + bestMatchingCb.expectAvailableDoubleValidatedCallbacks(mCellNetworkAgent); + + // Verify the callback doesn't care about wifi disconnect. + mWiFiNetworkAgent.disconnect(); + bestMatchingCb.assertNoCallback(); + mCellNetworkAgent.disconnect(); + bestMatchingCb.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + } + + private UidRangeParcel[] uidRangeFor(final UserHandle handle) { + UidRange range = UidRange.createForUser(handle); + return new UidRangeParcel[] { new UidRangeParcel(range.start, range.stop) }; + } + + private static class TestOnCompleteListener implements Runnable { + final class OnComplete {} + final ArrayTrackRecord.ReadHead mHistory = + new ArrayTrackRecord().newReadHead(); + + @Override + public void run() { + mHistory.add(new OnComplete()); + } + + public void expectOnComplete() { + assertNotNull(mHistory.poll(TIMEOUT_MS, it -> true)); + } + } + + private TestNetworkAgentWrapper makeEnterpriseNetworkAgent() throws Exception { + final NetworkCapabilities workNc = new NetworkCapabilities(); + workNc.addCapability(NET_CAPABILITY_ENTERPRISE); + workNc.removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + return new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, new LinkProperties(), workNc); + } + + private TestNetworkCallback mEnterpriseCallback; + private UserHandle setupEnterpriseNetwork() { + final UserHandle userHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID); + mServiceContext.setWorkProfile(userHandle, true); + + // File a request to avoid the enterprise network being disconnected as soon as the default + // request goes away – it would make impossible to test that networkRemoveUidRanges + // is called, as the network would disconnect first for lack of a request. + mEnterpriseCallback = new TestNetworkCallback(); + final NetworkRequest keepUpRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_ENTERPRISE) + .build(); + mCm.requestNetwork(keepUpRequest, mEnterpriseCallback); + return userHandle; + } + + private void maybeTearDownEnterpriseNetwork() { + if (null != mEnterpriseCallback) { + mCm.unregisterNetworkCallback(mEnterpriseCallback); + } + } + + /** + * Make sure per-profile networking preference behaves as expected when the enterprise network + * goes up and down while the preference is active. Make sure they behave as expected whether + * there is a general default network or not. + */ + @Test + public void testPreferenceForUserNetworkUpDown() throws Exception { + final InOrder inOrder = inOrder(mMockNetd); + final UserHandle testHandle = setupEnterpriseNetwork(); + registerDefaultNetworkCallbacks(); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + + mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + mProfileDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical( + mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE)); + + + final TestOnCompleteListener listener = new TestOnCompleteListener(); + mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE, + r -> r.run(), listener); + listener.expectOnComplete(); + + // Setting a network preference for this user will create a new set of routing rules for + // the UID range that corresponds to this user, so as to define the default network + // for these apps separately. This is true because the multi-layer request relevant to + // this UID range contains a TRACK_DEFAULT, so the range will be moved through UID-specific + // rules to the correct network – in this case the system default network. The case where + // the default network for the profile happens to be the same as the system default + // is not handled specially, the rules are always active as long as a preference is set. + inOrder.verify(mMockNetd).networkAddUidRanges(mCellNetworkAgent.getNetwork().netId, + uidRangeFor(testHandle)); + + // The enterprise network is not ready yet. + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback, + mProfileDefaultNetworkCallback); + + final TestNetworkAgentWrapper workAgent = makeEnterpriseNetworkAgent(); + workAgent.connect(false); + + mProfileDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(workAgent); + mSystemDefaultNetworkCallback.assertNoCallback(); + mDefaultNetworkCallback.assertNoCallback(); + inOrder.verify(mMockNetd).networkCreate( + nativeNetworkConfigPhysical(workAgent.getNetwork().netId, INetd.PERMISSION_SYSTEM)); + inOrder.verify(mMockNetd).networkAddUidRanges(workAgent.getNetwork().netId, + uidRangeFor(testHandle)); + inOrder.verify(mMockNetd).networkRemoveUidRanges(mCellNetworkAgent.getNetwork().netId, + uidRangeFor(testHandle)); + + // Make sure changes to the work agent send callbacks to the app in the work profile, but + // not to the other apps. + workAgent.setNetworkValid(true /* isStrictMode */); + workAgent.mNetworkMonitor.forceReevaluation(Process.myUid()); + mProfileDefaultNetworkCallback.expectCapabilitiesThat(workAgent, + nc -> nc.hasCapability(NET_CAPABILITY_VALIDATED) + && nc.hasCapability(NET_CAPABILITY_ENTERPRISE)); + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback); + + workAgent.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED); + mProfileDefaultNetworkCallback.expectCapabilitiesThat(workAgent, nc -> + nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)); + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback); + + // Conversely, change a capability on the system-wide default network and make sure + // that only the apps outside of the work profile receive the callbacks. + mCellNetworkAgent.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED); + mSystemDefaultNetworkCallback.expectCapabilitiesThat(mCellNetworkAgent, nc -> + nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)); + mDefaultNetworkCallback.expectCapabilitiesThat(mCellNetworkAgent, nc -> + nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)); + mProfileDefaultNetworkCallback.assertNoCallback(); + + // Disconnect and reconnect the system-wide default network and make sure that the + // apps on this network see the appropriate callbacks, and the app on the work profile + // doesn't because it continues to use the enterprise network. + mCellNetworkAgent.disconnect(); + mSystemDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + mDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + mProfileDefaultNetworkCallback.assertNoCallback(); + inOrder.verify(mMockNetd).networkDestroy(mCellNetworkAgent.getNetwork().netId); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + mProfileDefaultNetworkCallback.assertNoCallback(); + inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical( + mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE)); + + // When the agent disconnects, test that the app on the work profile falls back to the + // default network. + workAgent.disconnect(); + mProfileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, workAgent); + mProfileDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback); + inOrder.verify(mMockNetd).networkAddUidRanges(mCellNetworkAgent.getNetwork().netId, + uidRangeFor(testHandle)); + inOrder.verify(mMockNetd).networkDestroy(workAgent.getNetwork().netId); + + mCellNetworkAgent.disconnect(); + mSystemDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + mDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + mProfileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + + // Waiting for the handler to be idle before checking for networkDestroy is necessary + // here because ConnectivityService calls onLost before the network is fully torn down. + waitForIdle(); + inOrder.verify(mMockNetd).networkDestroy(mCellNetworkAgent.getNetwork().netId); + + // If the control comes here, callbacks seem to behave correctly in the presence of + // a default network when the enterprise network goes up and down. Now, make sure they + // also behave correctly in the absence of a system-wide default network. + final TestNetworkAgentWrapper workAgent2 = makeEnterpriseNetworkAgent(); + workAgent2.connect(false); + + mProfileDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(workAgent2); + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback); + inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical( + workAgent2.getNetwork().netId, INetd.PERMISSION_SYSTEM)); + inOrder.verify(mMockNetd).networkAddUidRanges(workAgent2.getNetwork().netId, + uidRangeFor(testHandle)); + + workAgent2.setNetworkValid(true /* isStrictMode */); + workAgent2.mNetworkMonitor.forceReevaluation(Process.myUid()); + mProfileDefaultNetworkCallback.expectCapabilitiesThat(workAgent2, + nc -> nc.hasCapability(NET_CAPABILITY_ENTERPRISE) + && !nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback); + inOrder.verify(mMockNetd, never()).networkAddUidRanges(anyInt(), any()); + + // When the agent disconnects, test that the app on the work profile falls back to the + // default network. + workAgent2.disconnect(); + mProfileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, workAgent2); + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback); + inOrder.verify(mMockNetd).networkDestroy(workAgent2.getNetwork().netId); + + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback, + mProfileDefaultNetworkCallback); + + // Callbacks will be unregistered by tearDown() + } + + /** + * Test that, in a given networking context, calling setPreferenceForUser to set per-profile + * defaults on then off works as expected. + */ + @Test + public void testSetPreferenceForUserOnOff() throws Exception { + final InOrder inOrder = inOrder(mMockNetd); + final UserHandle testHandle = setupEnterpriseNetwork(); + + // Connect both a regular cell agent and an enterprise network first. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + + final TestNetworkAgentWrapper workAgent = makeEnterpriseNetworkAgent(); + workAgent.connect(true); + + final TestOnCompleteListener listener = new TestOnCompleteListener(); + mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE, + r -> r.run(), listener); + listener.expectOnComplete(); + inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical( + mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE)); + inOrder.verify(mMockNetd).networkAddUidRanges(workAgent.getNetwork().netId, + uidRangeFor(testHandle)); + + registerDefaultNetworkCallbacks(); + + mSystemDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + mDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + mProfileDefaultNetworkCallback.expectAvailableCallbacksValidated(workAgent); + + mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_DEFAULT, + r -> r.run(), listener); + listener.expectOnComplete(); + + mProfileDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback); + inOrder.verify(mMockNetd).networkRemoveUidRanges(workAgent.getNetwork().netId, + uidRangeFor(testHandle)); + + workAgent.disconnect(); + mCellNetworkAgent.disconnect(); + + // Callbacks will be unregistered by tearDown() + } + + /** + * Test per-profile default networks for two different profiles concurrently. + */ + @Test + public void testSetPreferenceForTwoProfiles() throws Exception { + final InOrder inOrder = inOrder(mMockNetd); + final UserHandle testHandle2 = setupEnterpriseNetwork(); + final UserHandle testHandle4 = UserHandle.of(TEST_WORK_PROFILE_USER_ID + 2); + mServiceContext.setWorkProfile(testHandle4, true); + registerDefaultNetworkCallbacks(); + + final TestNetworkCallback app4Cb = new TestNetworkCallback(); + final int testWorkProfileAppUid4 = + UserHandle.getUid(testHandle4.getIdentifier(), TEST_APP_ID); + registerDefaultNetworkCallbackAsUid(app4Cb, testWorkProfileAppUid4); + + // Connect both a regular cell agent and an enterprise network first. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + + final TestNetworkAgentWrapper workAgent = makeEnterpriseNetworkAgent(); + workAgent.connect(true); + + mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + mProfileDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + app4Cb.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical( + mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE)); + inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical( + workAgent.getNetwork().netId, INetd.PERMISSION_SYSTEM)); + + final TestOnCompleteListener listener = new TestOnCompleteListener(); + mCm.setProfileNetworkPreference(testHandle2, PROFILE_NETWORK_PREFERENCE_ENTERPRISE, + r -> r.run(), listener); + listener.expectOnComplete(); + inOrder.verify(mMockNetd).networkAddUidRanges(workAgent.getNetwork().netId, + uidRangeFor(testHandle2)); + + mProfileDefaultNetworkCallback.expectAvailableCallbacksValidated(workAgent); + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback, + app4Cb); + + mCm.setProfileNetworkPreference(testHandle4, PROFILE_NETWORK_PREFERENCE_ENTERPRISE, + r -> r.run(), listener); + listener.expectOnComplete(); + inOrder.verify(mMockNetd).networkAddUidRanges(workAgent.getNetwork().netId, + uidRangeFor(testHandle4)); + + app4Cb.expectAvailableCallbacksValidated(workAgent); + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback, + mProfileDefaultNetworkCallback); + + mCm.setProfileNetworkPreference(testHandle2, PROFILE_NETWORK_PREFERENCE_DEFAULT, + r -> r.run(), listener); + listener.expectOnComplete(); + inOrder.verify(mMockNetd).networkRemoveUidRanges(workAgent.getNetwork().netId, + uidRangeFor(testHandle2)); + + mProfileDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback, + app4Cb); + + workAgent.disconnect(); + mCellNetworkAgent.disconnect(); + + mCm.unregisterNetworkCallback(app4Cb); + // Other callbacks will be unregistered by tearDown() + } + + @Test + public void testProfilePreferenceRemovedUponUserRemoved() throws Exception { + final InOrder inOrder = inOrder(mMockNetd); + final UserHandle testHandle = setupEnterpriseNetwork(); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + + final TestOnCompleteListener listener = new TestOnCompleteListener(); + mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE, + r -> r.run(), listener); + listener.expectOnComplete(); + inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical( + mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE)); + inOrder.verify(mMockNetd).networkAddUidRanges(mCellNetworkAgent.getNetwork().netId, + uidRangeFor(testHandle)); + + final Intent removedIntent = new Intent(ACTION_USER_REMOVED); + removedIntent.putExtra(Intent.EXTRA_USER, testHandle); + processBroadcast(removedIntent); + + inOrder.verify(mMockNetd).networkRemoveUidRanges(mCellNetworkAgent.getNetwork().netId, + uidRangeFor(testHandle)); + } + + /** + * Make sure that OEM preference and per-profile preference can't be used at the same + * time and throw ISE if tried + */ + @Test + public void testOemPreferenceAndProfilePreferenceExclusive() throws Exception { + final UserHandle testHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID); + mServiceContext.setWorkProfile(testHandle, true); + final TestOnCompleteListener listener = new TestOnCompleteListener(); + + setupMultipleDefaultNetworksForOemNetworkPreferenceNotCurrentUidTest( + OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY); + assertThrows("Should not be able to set per-profile pref while OEM prefs present", + IllegalStateException.class, () -> + mCm.setProfileNetworkPreference(testHandle, + PROFILE_NETWORK_PREFERENCE_ENTERPRISE, + r -> r.run(), listener)); + + // Empty the OEM prefs + final TestOemListenerCallback oemPrefListener = new TestOemListenerCallback(); + final OemNetworkPreferences emptyOemPref = new OemNetworkPreferences.Builder().build(); + mService.setOemNetworkPreference(emptyOemPref, oemPrefListener); + oemPrefListener.expectOnComplete(); + + mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE, + r -> r.run(), listener); + listener.expectOnComplete(); + assertThrows("Should not be able to set OEM prefs while per-profile pref is on", + IllegalStateException.class , () -> + mService.setOemNetworkPreference(emptyOemPref, oemPrefListener)); + } + + /** + * Make sure wrong preferences for per-profile default networking are rejected. + */ + @Test + public void testProfileNetworkPrefWrongPreference() throws Exception { + final UserHandle testHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID); + mServiceContext.setWorkProfile(testHandle, true); + assertThrows("Should not be able to set an illegal preference", + IllegalArgumentException.class, + () -> mCm.setProfileNetworkPreference(testHandle, + PROFILE_NETWORK_PREFERENCE_ENTERPRISE + 1, null, null)); + } + + /** + * Make sure requests for per-profile default networking for a non-work profile are + * rejected + */ + @Test + public void testProfileNetworkPrefWrongProfile() throws Exception { + final UserHandle testHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID); + mServiceContext.setWorkProfile(testHandle, false); + assertThrows("Should not be able to set a user pref for a non-work profile", + IllegalArgumentException.class , () -> + mCm.setProfileNetworkPreference(testHandle, + PROFILE_NETWORK_PREFERENCE_ENTERPRISE, null, null)); + } + + @Test + public void testSubIdsClearedWithoutNetworkFactoryPermission() throws Exception { + mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_DENIED); + final NetworkCapabilities nc = new NetworkCapabilities(); + nc.setSubscriptionIds(Collections.singleton(Process.myUid())); + + final NetworkCapabilities result = + mService.networkCapabilitiesRestrictedForCallerPermissions( + nc, Process.myPid(), Process.myUid()); + assertTrue(result.getSubscriptionIds().isEmpty()); + } + + @Test + public void testSubIdsExistWithNetworkFactoryPermission() throws Exception { + mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED); + + final Set subIds = Collections.singleton(Process.myUid()); + final NetworkCapabilities nc = new NetworkCapabilities(); + nc.setSubscriptionIds(subIds); + + final NetworkCapabilities result = + mService.networkCapabilitiesRestrictedForCallerPermissions( + nc, Process.myPid(), Process.myUid()); + assertEquals(subIds, result.getSubscriptionIds()); + } + + private NetworkRequest getRequestWithSubIds() { + return new NetworkRequest.Builder() + .setSubscriptionIds(Collections.singleton(Process.myUid())) + .build(); + } + + @Test + public void testNetworkRequestWithSubIdsWithNetworkFactoryPermission() throws Exception { + mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED); + final PendingIntent pendingIntent = PendingIntent.getBroadcast( + mContext, 0 /* requestCode */, new Intent("a"), FLAG_IMMUTABLE); + final NetworkCallback networkCallback1 = new NetworkCallback(); + final NetworkCallback networkCallback2 = new NetworkCallback(); + + mCm.requestNetwork(getRequestWithSubIds(), networkCallback1); + mCm.requestNetwork(getRequestWithSubIds(), pendingIntent); + mCm.registerNetworkCallback(getRequestWithSubIds(), networkCallback2); + + mCm.unregisterNetworkCallback(networkCallback1); + mCm.releaseNetworkRequest(pendingIntent); + mCm.unregisterNetworkCallback(networkCallback2); + } + + @Test + public void testNetworkRequestWithSubIdsWithoutNetworkFactoryPermission() throws Exception { + mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_DENIED); + final PendingIntent pendingIntent = PendingIntent.getBroadcast( + mContext, 0 /* requestCode */, new Intent("a"), FLAG_IMMUTABLE); + + final Class expected = SecurityException.class; + assertThrows( + expected, () -> mCm.requestNetwork(getRequestWithSubIds(), new NetworkCallback())); + assertThrows(expected, () -> mCm.requestNetwork(getRequestWithSubIds(), pendingIntent)); + assertThrows( + expected, + () -> mCm.registerNetworkCallback(getRequestWithSubIds(), new NetworkCallback())); + } + + /** + * Validate request counts are counted accurately on setProfileNetworkPreference on set/replace. + */ + @Test + public void testProfileNetworkPrefCountsRequestsCorrectlyOnSet() throws Exception { + final UserHandle testHandle = setupEnterpriseNetwork(); + testRequestCountLimits(() -> { + // Set initially to test the limit prior to having existing requests. + final TestOnCompleteListener listener = new TestOnCompleteListener(); + mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE, + Runnable::run, listener); + listener.expectOnComplete(); + + // re-set so as to test the limit as part of replacing existing requests. + mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE, + Runnable::run, listener); + listener.expectOnComplete(); + }); + } + + /** + * Validate request counts are counted accurately on setOemNetworkPreference on set/replace. + */ + @Test + public void testSetOemNetworkPreferenceCountsRequestsCorrectlyOnSet() throws Exception { + mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, true); + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY; + testRequestCountLimits(() -> { + // Set initially to test the limit prior to having existing requests. + final TestOemListenerCallback listener = new TestOemListenerCallback(); + mService.setOemNetworkPreference( + createDefaultOemNetworkPreferences(networkPref), listener); + listener.expectOnComplete(); + + // re-set so as to test the limit as part of replacing existing requests. + mService.setOemNetworkPreference( + createDefaultOemNetworkPreferences(networkPref), listener); + listener.expectOnComplete(); + }); + } + + private void testRequestCountLimits(@NonNull final Runnable r) throws Exception { + final ArraySet callbacks = new ArraySet<>(); + try { + final int requestCount = mService.mSystemNetworkRequestCounter + .mUidToNetworkRequestCount.get(Process.myUid()); + // The limit is hit when total requests <= limit. + final int maxCount = + ConnectivityService.MAX_NETWORK_REQUESTS_PER_SYSTEM_UID - requestCount; + // Need permission so registerDefaultNetworkCallback uses mSystemNetworkRequestCounter + withPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, () -> { + for (int i = 1; i < maxCount - 1; i++) { + final TestNetworkCallback cb = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(cb); + callbacks.add(cb); + } + + // Code to run to check if it triggers a max request count limit error. + r.run(); + }); + } finally { + for (final TestNetworkCallback cb : callbacks) { + mCm.unregisterNetworkCallback(cb); + } + } + } +} diff --git a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java new file mode 100644 index 0000000000..cf2c9c783a --- /dev/null +++ b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java @@ -0,0 +1,1004 @@ +/* + * 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; + +import static android.content.pm.PackageManager.PERMISSION_DENIED; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.net.INetd.IF_STATE_DOWN; +import static android.net.INetd.IF_STATE_UP; +import static android.net.IpSecManager.DIRECTION_FWD; +import static android.net.IpSecManager.DIRECTION_IN; +import static android.net.IpSecManager.DIRECTION_OUT; +import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK; +import static android.system.OsConstants.AF_INET; +import static android.system.OsConstants.AF_INET6; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.AppOpsManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.INetd; +import android.net.InetAddresses; +import android.net.InterfaceConfigurationParcel; +import android.net.IpSecAlgorithm; +import android.net.IpSecConfig; +import android.net.IpSecManager; +import android.net.IpSecSpiResponse; +import android.net.IpSecTransform; +import android.net.IpSecTransformResponse; +import android.net.IpSecTunnelInterfaceResponse; +import android.net.IpSecUdpEncapResponse; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.Network; +import android.os.Binder; +import android.os.ParcelFileDescriptor; +import android.system.Os; +import android.test.mock.MockContext; +import android.util.ArraySet; + +import androidx.test.filters.SmallTest; + +import com.android.server.IpSecService.TunnelInterfaceRecord; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.net.Inet4Address; +import java.net.Socket; +import java.util.Arrays; +import java.util.Collection; +import java.util.Set; + +/** Unit tests for {@link IpSecService}. */ +@SmallTest +@RunWith(Parameterized.class) +public class IpSecServiceParameterizedTest { + + private static final int TEST_SPI = 0xD1201D; + + private final String mSourceAddr; + private final String mDestinationAddr; + private final LinkAddress mLocalInnerAddress; + private final int mFamily; + + private static final int[] ADDRESS_FAMILIES = + new int[] {AF_INET, AF_INET6}; + + @Parameterized.Parameters + public static Collection ipSecConfigs() { + return Arrays.asList( + new Object[][] { + {"1.2.3.4", "8.8.4.4", "10.0.1.1/24", AF_INET}, + {"2601::2", "2601::10", "2001:db8::1/64", AF_INET6} + }); + } + + private static final byte[] AEAD_KEY = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + 0x73, 0x61, 0x6C, 0x74 + }; + private static final byte[] CRYPT_KEY = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F + }; + private static final byte[] AUTH_KEY = { + 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, + 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F + }; + + AppOpsManager mMockAppOps = mock(AppOpsManager.class); + ConnectivityManager mMockConnectivityMgr = mock(ConnectivityManager.class); + + TestContext mTestContext = new TestContext(); + + private class TestContext extends MockContext { + private Set mAllowedPermissions = new ArraySet<>(Arrays.asList( + android.Manifest.permission.MANAGE_IPSEC_TUNNELS, + android.Manifest.permission.NETWORK_STACK, + PERMISSION_MAINLINE_NETWORK_STACK)); + + private void setAllowedPermissions(String... permissions) { + mAllowedPermissions = new ArraySet<>(permissions); + } + + @Override + public Object getSystemService(String name) { + switch(name) { + case Context.APP_OPS_SERVICE: + return mMockAppOps; + case Context.CONNECTIVITY_SERVICE: + return mMockConnectivityMgr; + default: + return null; + } + } + + @Override + public String getSystemServiceName(Class serviceClass) { + if (ConnectivityManager.class == serviceClass) { + return Context.CONNECTIVITY_SERVICE; + } + return null; + } + + @Override + public PackageManager getPackageManager() { + return mMockPkgMgr; + } + + @Override + public void enforceCallingOrSelfPermission(String permission, String message) { + if (mAllowedPermissions.contains(permission)) { + return; + } else { + throw new SecurityException("Unavailable permission requested"); + } + } + + @Override + public int checkCallingOrSelfPermission(String permission) { + if (mAllowedPermissions.contains(permission)) { + return PERMISSION_GRANTED; + } else { + return PERMISSION_DENIED; + } + } + } + + INetd mMockNetd; + PackageManager mMockPkgMgr; + IpSecService.IpSecServiceConfiguration mMockIpSecSrvConfig; + IpSecService mIpSecService; + Network fakeNetwork = new Network(0xAB); + int mUid = Os.getuid(); + + private static final IpSecAlgorithm AUTH_ALGO = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, AUTH_KEY, AUTH_KEY.length * 4); + private static final IpSecAlgorithm CRYPT_ALGO = + new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + private static final IpSecAlgorithm AEAD_ALGO = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128); + private static final int REMOTE_ENCAP_PORT = 4500; + + private static final String BLESSED_PACKAGE = "blessedPackage"; + private static final String SYSTEM_PACKAGE = "systemPackage"; + private static final String BAD_PACKAGE = "badPackage"; + + public IpSecServiceParameterizedTest( + String sourceAddr, String destAddr, String localInnerAddr, int family) { + mSourceAddr = sourceAddr; + mDestinationAddr = destAddr; + mLocalInnerAddress = new LinkAddress(localInnerAddr); + mFamily = family; + } + + @Before + public void setUp() throws Exception { + mMockNetd = mock(INetd.class); + mMockPkgMgr = mock(PackageManager.class); + mMockIpSecSrvConfig = mock(IpSecService.IpSecServiceConfiguration.class); + mIpSecService = new IpSecService(mTestContext, mMockIpSecSrvConfig); + + // Injecting mock netd + when(mMockIpSecSrvConfig.getNetdInstance()).thenReturn(mMockNetd); + + // PackageManager should always return true (feature flag tests in IpSecServiceTest) + when(mMockPkgMgr.hasSystemFeature(anyString())).thenReturn(true); + + // A package granted the AppOp for MANAGE_IPSEC_TUNNELS will be MODE_ALLOWED. + when(mMockAppOps.noteOp(anyInt(), anyInt(), eq(BLESSED_PACKAGE))) + .thenReturn(AppOpsManager.MODE_ALLOWED); + // A system package will not be granted the app op, so this should fall back to + // a permissions check, which should pass. + when(mMockAppOps.noteOp(anyInt(), anyInt(), eq(SYSTEM_PACKAGE))) + .thenReturn(AppOpsManager.MODE_DEFAULT); + // A mismatch between the package name and the UID will return MODE_IGNORED. + when(mMockAppOps.noteOp(anyInt(), anyInt(), eq(BAD_PACKAGE))) + .thenReturn(AppOpsManager.MODE_IGNORED); + } + + //TODO: Add a test to verify SPI. + + @Test + public void testIpSecServiceReserveSpi() throws Exception { + when(mMockNetd.ipSecAllocateSpi(anyInt(), anyString(), eq(mDestinationAddr), eq(TEST_SPI))) + .thenReturn(TEST_SPI); + + IpSecSpiResponse spiResp = + mIpSecService.allocateSecurityParameterIndex( + mDestinationAddr, TEST_SPI, new Binder()); + assertEquals(IpSecManager.Status.OK, spiResp.status); + assertEquals(TEST_SPI, spiResp.spi); + } + + @Test + public void testReleaseSecurityParameterIndex() throws Exception { + when(mMockNetd.ipSecAllocateSpi(anyInt(), anyString(), eq(mDestinationAddr), eq(TEST_SPI))) + .thenReturn(TEST_SPI); + + IpSecSpiResponse spiResp = + mIpSecService.allocateSecurityParameterIndex( + mDestinationAddr, TEST_SPI, new Binder()); + + mIpSecService.releaseSecurityParameterIndex(spiResp.resourceId); + + verify(mMockNetd) + .ipSecDeleteSecurityAssociation( + eq(mUid), + anyString(), + anyString(), + eq(TEST_SPI), + anyInt(), + anyInt(), + anyInt()); + + // Verify quota and RefcountedResource objects cleaned up + IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid); + assertEquals(0, userRecord.mSpiQuotaTracker.mCurrent); + try { + userRecord.mSpiRecords.getRefcountedResourceOrThrow(spiResp.resourceId); + fail("Expected IllegalArgumentException on attempt to access deleted resource"); + } catch (IllegalArgumentException expected) { + + } + } + + @Test + public void testSecurityParameterIndexBinderDeath() throws Exception { + when(mMockNetd.ipSecAllocateSpi(anyInt(), anyString(), eq(mDestinationAddr), eq(TEST_SPI))) + .thenReturn(TEST_SPI); + + IpSecSpiResponse spiResp = + mIpSecService.allocateSecurityParameterIndex( + mDestinationAddr, TEST_SPI, new Binder()); + + IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid); + IpSecService.RefcountedResource refcountedRecord = + userRecord.mSpiRecords.getRefcountedResourceOrThrow(spiResp.resourceId); + + refcountedRecord.binderDied(); + + verify(mMockNetd) + .ipSecDeleteSecurityAssociation( + eq(mUid), + anyString(), + anyString(), + eq(TEST_SPI), + anyInt(), + anyInt(), + anyInt()); + + // Verify quota and RefcountedResource objects cleaned up + assertEquals(0, userRecord.mSpiQuotaTracker.mCurrent); + try { + userRecord.mSpiRecords.getRefcountedResourceOrThrow(spiResp.resourceId); + fail("Expected IllegalArgumentException on attempt to access deleted resource"); + } catch (IllegalArgumentException expected) { + + } + } + + private int getNewSpiResourceId(String remoteAddress, int returnSpi) throws Exception { + when(mMockNetd.ipSecAllocateSpi(anyInt(), anyString(), anyString(), anyInt())) + .thenReturn(returnSpi); + + IpSecSpiResponse spi = + mIpSecService.allocateSecurityParameterIndex( + InetAddresses.parseNumericAddress(remoteAddress).getHostAddress(), + IpSecManager.INVALID_SECURITY_PARAMETER_INDEX, + new Binder()); + return spi.resourceId; + } + + private void addDefaultSpisAndRemoteAddrToIpSecConfig(IpSecConfig config) throws Exception { + config.setSpiResourceId(getNewSpiResourceId(mDestinationAddr, TEST_SPI)); + config.setSourceAddress(mSourceAddr); + config.setDestinationAddress(mDestinationAddr); + } + + private void addAuthAndCryptToIpSecConfig(IpSecConfig config) throws Exception { + config.setEncryption(CRYPT_ALGO); + config.setAuthentication(AUTH_ALGO); + } + + private void addEncapSocketToIpSecConfig(int resourceId, IpSecConfig config) throws Exception { + config.setEncapType(IpSecTransform.ENCAP_ESPINUDP); + config.setEncapSocketResourceId(resourceId); + config.setEncapRemotePort(REMOTE_ENCAP_PORT); + } + + private void verifyTransformNetdCalledForCreatingSA( + IpSecConfig config, IpSecTransformResponse resp) throws Exception { + verifyTransformNetdCalledForCreatingSA(config, resp, 0); + } + + private void verifyTransformNetdCalledForCreatingSA( + IpSecConfig config, IpSecTransformResponse resp, int encapSocketPort) throws Exception { + IpSecAlgorithm auth = config.getAuthentication(); + IpSecAlgorithm crypt = config.getEncryption(); + IpSecAlgorithm authCrypt = config.getAuthenticatedEncryption(); + + verify(mMockNetd, times(1)) + .ipSecAddSecurityAssociation( + eq(mUid), + eq(config.getMode()), + eq(config.getSourceAddress()), + eq(config.getDestinationAddress()), + eq((config.getNetwork() != null) ? config.getNetwork().netId : 0), + eq(TEST_SPI), + eq(0), + eq(0), + eq((auth != null) ? auth.getName() : ""), + eq((auth != null) ? auth.getKey() : new byte[] {}), + eq((auth != null) ? auth.getTruncationLengthBits() : 0), + eq((crypt != null) ? crypt.getName() : ""), + eq((crypt != null) ? crypt.getKey() : new byte[] {}), + eq((crypt != null) ? crypt.getTruncationLengthBits() : 0), + eq((authCrypt != null) ? authCrypt.getName() : ""), + eq((authCrypt != null) ? authCrypt.getKey() : new byte[] {}), + eq((authCrypt != null) ? authCrypt.getTruncationLengthBits() : 0), + eq(config.getEncapType()), + eq(encapSocketPort), + eq(config.getEncapRemotePort()), + eq(config.getXfrmInterfaceId())); + } + + @Test + public void testCreateTransform() throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + assertEquals(IpSecManager.Status.OK, createTransformResp.status); + + verifyTransformNetdCalledForCreatingSA(ipSecConfig, createTransformResp); + } + + @Test + public void testCreateTransformAead() throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + + ipSecConfig.setAuthenticatedEncryption(AEAD_ALGO); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + assertEquals(IpSecManager.Status.OK, createTransformResp.status); + + verifyTransformNetdCalledForCreatingSA(ipSecConfig, createTransformResp); + } + + @Test + public void testCreateTransportModeTransformWithEncap() throws Exception { + IpSecUdpEncapResponse udpSock = mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + + IpSecConfig ipSecConfig = new IpSecConfig(); + ipSecConfig.setMode(IpSecTransform.MODE_TRANSPORT); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + addEncapSocketToIpSecConfig(udpSock.resourceId, ipSecConfig); + + if (mFamily == AF_INET) { + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + assertEquals(IpSecManager.Status.OK, createTransformResp.status); + + verifyTransformNetdCalledForCreatingSA(ipSecConfig, createTransformResp, udpSock.port); + } else { + try { + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + fail("Expected IllegalArgumentException on attempt to use UDP Encap in IPv6"); + } catch (IllegalArgumentException expected) { + } + } + } + + @Test + public void testCreateTunnelModeTransformWithEncap() throws Exception { + IpSecUdpEncapResponse udpSock = mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + + IpSecConfig ipSecConfig = new IpSecConfig(); + ipSecConfig.setMode(IpSecTransform.MODE_TUNNEL); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + addEncapSocketToIpSecConfig(udpSock.resourceId, ipSecConfig); + + if (mFamily == AF_INET) { + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + assertEquals(IpSecManager.Status.OK, createTransformResp.status); + + verifyTransformNetdCalledForCreatingSA(ipSecConfig, createTransformResp, udpSock.port); + } else { + try { + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + fail("Expected IllegalArgumentException on attempt to use UDP Encap in IPv6"); + } catch (IllegalArgumentException expected) { + } + } + } + + @Test + public void testCreateTwoTransformsWithSameSpis() throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + assertEquals(IpSecManager.Status.OK, createTransformResp.status); + + // Attempting to create transform a second time with the same SPIs should throw an error... + try { + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + fail("IpSecService should have thrown an error for reuse of SPI"); + } catch (IllegalStateException expected) { + } + + // ... even if the transform is deleted + mIpSecService.deleteTransform(createTransformResp.resourceId); + try { + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + fail("IpSecService should have thrown an error for reuse of SPI"); + } catch (IllegalStateException expected) { + } + } + + @Test + public void testReleaseOwnedSpi() throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid); + assertEquals(1, userRecord.mSpiQuotaTracker.mCurrent); + mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId()); + verify(mMockNetd, times(0)) + .ipSecDeleteSecurityAssociation( + eq(mUid), + anyString(), + anyString(), + eq(TEST_SPI), + anyInt(), + anyInt(), + anyInt()); + // quota is not released until the SPI is released by the Transform + assertEquals(1, userRecord.mSpiQuotaTracker.mCurrent); + } + + @Test + public void testDeleteTransform() throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + mIpSecService.deleteTransform(createTransformResp.resourceId); + + verify(mMockNetd, times(1)) + .ipSecDeleteSecurityAssociation( + eq(mUid), + anyString(), + anyString(), + eq(TEST_SPI), + anyInt(), + anyInt(), + anyInt()); + + // Verify quota and RefcountedResource objects cleaned up + IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid); + assertEquals(0, userRecord.mTransformQuotaTracker.mCurrent); + assertEquals(1, userRecord.mSpiQuotaTracker.mCurrent); + + mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId()); + // Verify that ipSecDeleteSa was not called when the SPI was released because the + // ownedByTransform property should prevent it; (note, the called count is cumulative). + verify(mMockNetd, times(1)) + .ipSecDeleteSecurityAssociation( + anyInt(), + anyString(), + anyString(), + anyInt(), + anyInt(), + anyInt(), + anyInt()); + assertEquals(0, userRecord.mSpiQuotaTracker.mCurrent); + + try { + userRecord.mTransformRecords.getRefcountedResourceOrThrow( + createTransformResp.resourceId); + fail("Expected IllegalArgumentException on attempt to access deleted resource"); + } catch (IllegalArgumentException expected) { + + } + } + + @Test + public void testTransportModeTransformBinderDeath() throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + + IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid); + IpSecService.RefcountedResource refcountedRecord = + userRecord.mTransformRecords.getRefcountedResourceOrThrow( + createTransformResp.resourceId); + + refcountedRecord.binderDied(); + + verify(mMockNetd) + .ipSecDeleteSecurityAssociation( + eq(mUid), + anyString(), + anyString(), + eq(TEST_SPI), + anyInt(), + anyInt(), + anyInt()); + + // Verify quota and RefcountedResource objects cleaned up + assertEquals(0, userRecord.mTransformQuotaTracker.mCurrent); + try { + userRecord.mTransformRecords.getRefcountedResourceOrThrow( + createTransformResp.resourceId); + fail("Expected IllegalArgumentException on attempt to access deleted resource"); + } catch (IllegalArgumentException expected) { + + } + } + + @Test + public void testApplyTransportModeTransform() throws Exception { + verifyApplyTransportModeTransformCommon(false); + } + + @Test + public void testApplyTransportModeTransformReleasedSpi() throws Exception { + verifyApplyTransportModeTransformCommon(true); + } + + public void verifyApplyTransportModeTransformCommon( + boolean closeSpiBeforeApply) throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + + if (closeSpiBeforeApply) { + mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId()); + } + + Socket socket = new Socket(); + socket.bind(null); + ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket); + + int resourceId = createTransformResp.resourceId; + mIpSecService.applyTransportModeTransform(pfd, IpSecManager.DIRECTION_OUT, resourceId); + + verify(mMockNetd) + .ipSecApplyTransportModeTransform( + eq(pfd), + eq(mUid), + eq(IpSecManager.DIRECTION_OUT), + anyString(), + anyString(), + eq(TEST_SPI)); + } + + @Test + public void testApplyTransportModeTransformWithClosedSpi() throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + + // Close SPI record + mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId()); + + Socket socket = new Socket(); + socket.bind(null); + ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket); + + int resourceId = createTransformResp.resourceId; + mIpSecService.applyTransportModeTransform(pfd, IpSecManager.DIRECTION_OUT, resourceId); + + verify(mMockNetd) + .ipSecApplyTransportModeTransform( + eq(pfd), + eq(mUid), + eq(IpSecManager.DIRECTION_OUT), + anyString(), + anyString(), + eq(TEST_SPI)); + } + + @Test + public void testRemoveTransportModeTransform() throws Exception { + Socket socket = new Socket(); + socket.bind(null); + ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket); + mIpSecService.removeTransportModeTransforms(pfd); + + verify(mMockNetd).ipSecRemoveTransportModeTransform(pfd); + } + + private IpSecTunnelInterfaceResponse createAndValidateTunnel( + String localAddr, String remoteAddr, String pkgName) throws Exception { + final InterfaceConfigurationParcel config = new InterfaceConfigurationParcel(); + config.flags = new String[] {IF_STATE_DOWN}; + when(mMockNetd.interfaceGetCfg(anyString())).thenReturn(config); + IpSecTunnelInterfaceResponse createTunnelResp = + mIpSecService.createTunnelInterface( + mSourceAddr, mDestinationAddr, fakeNetwork, new Binder(), pkgName); + + assertNotNull(createTunnelResp); + assertEquals(IpSecManager.Status.OK, createTunnelResp.status); + for (int direction : new int[] {DIRECTION_IN, DIRECTION_OUT, DIRECTION_FWD}) { + for (int selAddrFamily : ADDRESS_FAMILIES) { + verify(mMockNetd).ipSecAddSecurityPolicy( + eq(mUid), + eq(selAddrFamily), + eq(direction), + anyString(), + anyString(), + eq(0), + anyInt(), // iKey/oKey + anyInt(), // mask + eq(createTunnelResp.resourceId)); + } + } + + return createTunnelResp; + } + + @Test + public void testCreateTunnelInterface() throws Exception { + IpSecTunnelInterfaceResponse createTunnelResp = + createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE); + + // Check that we have stored the tracking object, and retrieve it + IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid); + IpSecService.RefcountedResource refcountedRecord = + userRecord.mTunnelInterfaceRecords.getRefcountedResourceOrThrow( + createTunnelResp.resourceId); + + assertEquals(1, userRecord.mTunnelQuotaTracker.mCurrent); + verify(mMockNetd) + .ipSecAddTunnelInterface( + eq(createTunnelResp.interfaceName), + eq(mSourceAddr), + eq(mDestinationAddr), + anyInt(), + anyInt(), + anyInt()); + verify(mMockNetd).interfaceSetCfg(argThat( + config -> Arrays.asList(config.flags).contains(IF_STATE_UP))); + } + + @Test + public void testDeleteTunnelInterface() throws Exception { + IpSecTunnelInterfaceResponse createTunnelResp = + createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE); + + IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid); + + mIpSecService.deleteTunnelInterface(createTunnelResp.resourceId, BLESSED_PACKAGE); + + // Verify quota and RefcountedResource objects cleaned up + assertEquals(0, userRecord.mTunnelQuotaTracker.mCurrent); + verify(mMockNetd).ipSecRemoveTunnelInterface(eq(createTunnelResp.interfaceName)); + try { + userRecord.mTunnelInterfaceRecords.getRefcountedResourceOrThrow( + createTunnelResp.resourceId); + fail("Expected IllegalArgumentException on attempt to access deleted resource"); + } catch (IllegalArgumentException expected) { + } + } + + private Network createFakeUnderlyingNetwork(String interfaceName) { + final Network fakeNetwork = new Network(1000); + final LinkProperties fakeLp = new LinkProperties(); + fakeLp.setInterfaceName(interfaceName); + when(mMockConnectivityMgr.getLinkProperties(eq(fakeNetwork))).thenReturn(fakeLp); + return fakeNetwork; + } + + @Test + public void testSetNetworkForTunnelInterface() throws Exception { + final IpSecTunnelInterfaceResponse createTunnelResp = + createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE); + final Network newFakeNetwork = createFakeUnderlyingNetwork("newFakeNetworkInterface"); + final int tunnelIfaceResourceId = createTunnelResp.resourceId; + mIpSecService.setNetworkForTunnelInterface( + tunnelIfaceResourceId, newFakeNetwork, BLESSED_PACKAGE); + + final IpSecService.UserRecord userRecord = + mIpSecService.mUserResourceTracker.getUserRecord(mUid); + assertEquals(1, userRecord.mTunnelQuotaTracker.mCurrent); + + final TunnelInterfaceRecord tunnelInterfaceInfo = + userRecord.mTunnelInterfaceRecords.getResourceOrThrow(tunnelIfaceResourceId); + assertEquals(newFakeNetwork, tunnelInterfaceInfo.getUnderlyingNetwork()); + } + + @Test + public void testSetNetworkForTunnelInterfaceFailsForInvalidResourceId() throws Exception { + final IpSecTunnelInterfaceResponse createTunnelResp = + createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE); + final Network newFakeNetwork = new Network(1000); + + try { + mIpSecService.setNetworkForTunnelInterface( + IpSecManager.INVALID_RESOURCE_ID, newFakeNetwork, BLESSED_PACKAGE); + fail("Expected an IllegalArgumentException for invalid resource ID."); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testSetNetworkForTunnelInterfaceFailsWhenSettingTunnelNetwork() throws Exception { + final IpSecTunnelInterfaceResponse createTunnelResp = + createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE); + final int tunnelIfaceResourceId = createTunnelResp.resourceId; + final IpSecService.UserRecord userRecord = + mIpSecService.mUserResourceTracker.getUserRecord(mUid); + final TunnelInterfaceRecord tunnelInterfaceInfo = + userRecord.mTunnelInterfaceRecords.getResourceOrThrow(tunnelIfaceResourceId); + + final Network newFakeNetwork = + createFakeUnderlyingNetwork(tunnelInterfaceInfo.getInterfaceName()); + + try { + mIpSecService.setNetworkForTunnelInterface( + tunnelIfaceResourceId, newFakeNetwork, BLESSED_PACKAGE); + fail( + "Expected an IllegalArgumentException because the underlying network is the" + + " network being exposed by this tunnel."); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testTunnelInterfaceBinderDeath() throws Exception { + IpSecTunnelInterfaceResponse createTunnelResp = + createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE); + + IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid); + IpSecService.RefcountedResource refcountedRecord = + userRecord.mTunnelInterfaceRecords.getRefcountedResourceOrThrow( + createTunnelResp.resourceId); + + refcountedRecord.binderDied(); + + // Verify quota and RefcountedResource objects cleaned up + assertEquals(0, userRecord.mTunnelQuotaTracker.mCurrent); + verify(mMockNetd).ipSecRemoveTunnelInterface(eq(createTunnelResp.interfaceName)); + try { + userRecord.mTunnelInterfaceRecords.getRefcountedResourceOrThrow( + createTunnelResp.resourceId); + fail("Expected IllegalArgumentException on attempt to access deleted resource"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testApplyTunnelModeTransformOutbound() throws Exception { + verifyApplyTunnelModeTransformCommon(false /* closeSpiBeforeApply */, DIRECTION_OUT); + } + + @Test + public void testApplyTunnelModeTransformOutboundNonNetworkStack() throws Exception { + mTestContext.setAllowedPermissions(android.Manifest.permission.MANAGE_IPSEC_TUNNELS); + verifyApplyTunnelModeTransformCommon(false /* closeSpiBeforeApply */, DIRECTION_OUT); + } + + @Test + public void testApplyTunnelModeTransformOutboundReleasedSpi() throws Exception { + verifyApplyTunnelModeTransformCommon(true /* closeSpiBeforeApply */, DIRECTION_OUT); + } + + @Test + public void testApplyTunnelModeTransformInbound() throws Exception { + verifyApplyTunnelModeTransformCommon(true /* closeSpiBeforeApply */, DIRECTION_IN); + } + + @Test + public void testApplyTunnelModeTransformInboundNonNetworkStack() throws Exception { + mTestContext.setAllowedPermissions(android.Manifest.permission.MANAGE_IPSEC_TUNNELS); + verifyApplyTunnelModeTransformCommon(true /* closeSpiBeforeApply */, DIRECTION_IN); + } + + @Test + public void testApplyTunnelModeTransformForward() throws Exception { + verifyApplyTunnelModeTransformCommon(true /* closeSpiBeforeApply */, DIRECTION_FWD); + } + + @Test + public void testApplyTunnelModeTransformForwardNonNetworkStack() throws Exception { + mTestContext.setAllowedPermissions(android.Manifest.permission.MANAGE_IPSEC_TUNNELS); + + try { + verifyApplyTunnelModeTransformCommon(true /* closeSpiBeforeApply */, DIRECTION_FWD); + fail("Expected security exception due to use of forward policies without NETWORK_STACK" + + " or MAINLINE_NETWORK_STACK permission"); + } catch (SecurityException expected) { + } + } + + public void verifyApplyTunnelModeTransformCommon(boolean closeSpiBeforeApply, int direction) + throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + ipSecConfig.setMode(IpSecTransform.MODE_TUNNEL); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + IpSecTunnelInterfaceResponse createTunnelResp = + createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE); + + if (closeSpiBeforeApply) { + mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId()); + } + + int transformResourceId = createTransformResp.resourceId; + int tunnelResourceId = createTunnelResp.resourceId; + mIpSecService.applyTunnelModeTransform( + tunnelResourceId, direction, transformResourceId, BLESSED_PACKAGE); + + for (int selAddrFamily : ADDRESS_FAMILIES) { + verify(mMockNetd) + .ipSecUpdateSecurityPolicy( + eq(mUid), + eq(selAddrFamily), + eq(direction), + anyString(), + anyString(), + eq(direction == DIRECTION_OUT ? TEST_SPI : 0), + anyInt(), // iKey/oKey + anyInt(), // mask + eq(tunnelResourceId)); + } + + ipSecConfig.setXfrmInterfaceId(tunnelResourceId); + verifyTransformNetdCalledForCreatingSA(ipSecConfig, createTransformResp); + } + + + @Test + public void testApplyTunnelModeTransformWithClosedSpi() throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + ipSecConfig.setMode(IpSecTransform.MODE_TUNNEL); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + IpSecTunnelInterfaceResponse createTunnelResp = + createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE); + + // Close SPI record + mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId()); + + int transformResourceId = createTransformResp.resourceId; + int tunnelResourceId = createTunnelResp.resourceId; + mIpSecService.applyTunnelModeTransform( + tunnelResourceId, IpSecManager.DIRECTION_OUT, transformResourceId, BLESSED_PACKAGE); + + for (int selAddrFamily : ADDRESS_FAMILIES) { + verify(mMockNetd) + .ipSecUpdateSecurityPolicy( + eq(mUid), + eq(selAddrFamily), + eq(IpSecManager.DIRECTION_OUT), + anyString(), + anyString(), + eq(TEST_SPI), + anyInt(), // iKey/oKey + anyInt(), // mask + eq(tunnelResourceId)); + } + + ipSecConfig.setXfrmInterfaceId(tunnelResourceId); + verifyTransformNetdCalledForCreatingSA(ipSecConfig, createTransformResp); + } + + @Test + public void testAddRemoveAddressFromTunnelInterface() throws Exception { + for (String pkgName : new String[] {BLESSED_PACKAGE, SYSTEM_PACKAGE}) { + IpSecTunnelInterfaceResponse createTunnelResp = + createAndValidateTunnel(mSourceAddr, mDestinationAddr, pkgName); + mIpSecService.addAddressToTunnelInterface( + createTunnelResp.resourceId, mLocalInnerAddress, pkgName); + verify(mMockNetd, times(1)) + .interfaceAddAddress( + eq(createTunnelResp.interfaceName), + eq(mLocalInnerAddress.getAddress().getHostAddress()), + eq(mLocalInnerAddress.getPrefixLength())); + mIpSecService.removeAddressFromTunnelInterface( + createTunnelResp.resourceId, mLocalInnerAddress, pkgName); + verify(mMockNetd, times(1)) + .interfaceDelAddress( + eq(createTunnelResp.interfaceName), + eq(mLocalInnerAddress.getAddress().getHostAddress()), + eq(mLocalInnerAddress.getPrefixLength())); + mIpSecService.deleteTunnelInterface(createTunnelResp.resourceId, pkgName); + } + } + + @Ignore + @Test + public void testAddTunnelFailsForBadPackageName() throws Exception { + try { + IpSecTunnelInterfaceResponse createTunnelResp = + createAndValidateTunnel(mSourceAddr, mDestinationAddr, BAD_PACKAGE); + fail("Expected a SecurityException for badPackage."); + } catch (SecurityException expected) { + } + } + + @Test + public void testFeatureFlagVerification() throws Exception { + when(mMockPkgMgr.hasSystemFeature(eq(PackageManager.FEATURE_IPSEC_TUNNELS))) + .thenReturn(false); + + try { + String addr = Inet4Address.getLoopbackAddress().getHostAddress(); + mIpSecService.createTunnelInterface( + addr, addr, new Network(0), new Binder(), BLESSED_PACKAGE); + fail("Expected UnsupportedOperationException for disabled feature"); + } catch (UnsupportedOperationException expected) { + } + } +} diff --git a/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java b/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java new file mode 100644 index 0000000000..22a2c94fc1 --- /dev/null +++ b/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java @@ -0,0 +1,358 @@ +/* + * 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.os.Binder; +import android.os.IBinder; +import android.os.RemoteException; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.IpSecService.IResource; +import com.android.server.IpSecService.RefcountedResource; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; + +/** Unit tests for {@link IpSecService.RefcountedResource}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class IpSecServiceRefcountedResourceTest { + Context mMockContext; + IpSecService.IpSecServiceConfiguration mMockIpSecSrvConfig; + IpSecService mIpSecService; + + @Before + public void setUp() throws Exception { + mMockContext = mock(Context.class); + mMockIpSecSrvConfig = mock(IpSecService.IpSecServiceConfiguration.class); + mIpSecService = new IpSecService(mMockContext, mMockIpSecSrvConfig); + } + + private void assertResourceState( + RefcountedResource resource, + int refCount, + int userReleaseCallCount, + int releaseReferenceCallCount, + int invalidateCallCount, + int freeUnderlyingResourcesCallCount) + throws RemoteException { + // Check refcount on RefcountedResource + assertEquals(refCount, resource.mRefCount); + + // Check call count of RefcountedResource + verify(resource, times(userReleaseCallCount)).userRelease(); + verify(resource, times(releaseReferenceCallCount)).releaseReference(); + + // Check call count of IResource + verify(resource.getResource(), times(invalidateCallCount)).invalidate(); + verify(resource.getResource(), times(freeUnderlyingResourcesCallCount)) + .freeUnderlyingResources(); + } + + /** Adds mockito instrumentation */ + private RefcountedResource getTestRefcountedResource( + RefcountedResource... children) { + return getTestRefcountedResource(new Binder(), children); + } + + /** Adds mockito instrumentation with provided binder */ + private RefcountedResource getTestRefcountedResource( + IBinder binder, RefcountedResource... children) { + return spy( + mIpSecService + .new RefcountedResource(mock(IResource.class), binder, children)); + } + + @Test + public void testConstructor() throws RemoteException { + IBinder binderMock = mock(IBinder.class); + RefcountedResource resource = getTestRefcountedResource(binderMock); + + // Verify resource's refcount starts at 1 (for user-reference) + assertResourceState(resource, 1, 0, 0, 0, 0); + + // Verify linking to binder death + verify(binderMock).linkToDeath(anyObject(), anyInt()); + } + + @Test + public void testConstructorWithChildren() throws RemoteException { + IBinder binderMockChild = mock(IBinder.class); + IBinder binderMockParent = mock(IBinder.class); + RefcountedResource childResource = getTestRefcountedResource(binderMockChild); + RefcountedResource parentResource = + getTestRefcountedResource(binderMockParent, childResource); + + // Verify parent's refcount starts at 1 (for user-reference) + assertResourceState(parentResource, 1, 0, 0, 0, 0); + + // Verify child's refcounts were incremented + assertResourceState(childResource, 2, 0, 0, 0, 0); + + // Verify linking to binder death + verify(binderMockChild).linkToDeath(anyObject(), anyInt()); + verify(binderMockParent).linkToDeath(anyObject(), anyInt()); + } + + @Test + public void testFailLinkToDeath() throws RemoteException { + IBinder binderMock = mock(IBinder.class); + doThrow(new RemoteException()).when(binderMock).linkToDeath(anyObject(), anyInt()); + + try { + getTestRefcountedResource(binderMock); + fail("Expected exception to propogate when binder fails to link to death"); + } catch (RuntimeException expected) { + } + } + + @Test + public void testCleanupAndRelease() throws RemoteException { + IBinder binderMock = mock(IBinder.class); + RefcountedResource refcountedResource = getTestRefcountedResource(binderMock); + + // Verify user-initiated cleanup path decrements refcount and calls full cleanup flow + refcountedResource.userRelease(); + assertResourceState(refcountedResource, -1, 1, 1, 1, 1); + + // Verify user-initated cleanup path unlinks from binder + verify(binderMock).unlinkToDeath(eq(refcountedResource), eq(0)); + assertNull(refcountedResource.mBinder); + } + + @Test + public void testMultipleCallsToCleanupAndRelease() throws RemoteException { + RefcountedResource refcountedResource = getTestRefcountedResource(); + + // Verify calling userRelease multiple times does not trigger any other cleanup + // methods + refcountedResource.userRelease(); + assertResourceState(refcountedResource, -1, 1, 1, 1, 1); + + refcountedResource.userRelease(); + refcountedResource.userRelease(); + assertResourceState(refcountedResource, -1, 3, 1, 1, 1); + } + + @Test + public void testBinderDeathAfterCleanupAndReleaseDoesNothing() throws RemoteException { + RefcountedResource refcountedResource = getTestRefcountedResource(); + + refcountedResource.userRelease(); + assertResourceState(refcountedResource, -1, 1, 1, 1, 1); + + // Verify binder death call does not trigger any other cleanup methods if called after + // userRelease() + refcountedResource.binderDied(); + assertResourceState(refcountedResource, -1, 2, 1, 1, 1); + } + + @Test + public void testBinderDeath() throws RemoteException { + RefcountedResource refcountedResource = getTestRefcountedResource(); + + // Verify binder death caused cleanup + refcountedResource.binderDied(); + verify(refcountedResource, times(1)).binderDied(); + assertResourceState(refcountedResource, -1, 1, 1, 1, 1); + assertNull(refcountedResource.mBinder); + } + + @Test + public void testCleanupParentDecrementsChildRefcount() throws RemoteException { + RefcountedResource childResource = getTestRefcountedResource(); + RefcountedResource parentResource = getTestRefcountedResource(childResource); + + parentResource.userRelease(); + + // Verify parent gets cleaned up properly, and triggers releaseReference on + // child + assertResourceState(childResource, 1, 0, 1, 0, 0); + assertResourceState(parentResource, -1, 1, 1, 1, 1); + } + + @Test + public void testCleanupReferencedChildDoesNotTriggerRelease() throws RemoteException { + RefcountedResource childResource = getTestRefcountedResource(); + RefcountedResource parentResource = getTestRefcountedResource(childResource); + + childResource.userRelease(); + + // Verify that child does not clean up kernel resources and quota. + assertResourceState(childResource, 1, 1, 1, 1, 0); + assertResourceState(parentResource, 1, 0, 0, 0, 0); + } + + @Test + public void testTwoParents() throws RemoteException { + RefcountedResource childResource = getTestRefcountedResource(); + RefcountedResource parentResource1 = getTestRefcountedResource(childResource); + RefcountedResource parentResource2 = getTestRefcountedResource(childResource); + + // Verify that child does not cleanup kernel resources and quota until all references + // have been released. Assumption: parents release correctly based on + // testCleanupParentDecrementsChildRefcount() + childResource.userRelease(); + assertResourceState(childResource, 2, 1, 1, 1, 0); + + parentResource1.userRelease(); + assertResourceState(childResource, 1, 1, 2, 1, 0); + + parentResource2.userRelease(); + assertResourceState(childResource, -1, 1, 3, 1, 1); + } + + @Test + public void testTwoChildren() throws RemoteException { + RefcountedResource childResource1 = getTestRefcountedResource(); + RefcountedResource childResource2 = getTestRefcountedResource(); + RefcountedResource parentResource = + getTestRefcountedResource(childResource1, childResource2); + + childResource1.userRelease(); + assertResourceState(childResource1, 1, 1, 1, 1, 0); + assertResourceState(childResource2, 2, 0, 0, 0, 0); + + parentResource.userRelease(); + assertResourceState(childResource1, -1, 1, 2, 1, 1); + assertResourceState(childResource2, 1, 0, 1, 0, 0); + + childResource2.userRelease(); + assertResourceState(childResource1, -1, 1, 2, 1, 1); + assertResourceState(childResource2, -1, 1, 2, 1, 1); + } + + @Test + public void testSampleUdpEncapTranform() throws RemoteException { + RefcountedResource spi1 = getTestRefcountedResource(); + RefcountedResource spi2 = getTestRefcountedResource(); + RefcountedResource udpEncapSocket = getTestRefcountedResource(); + RefcountedResource transform = + getTestRefcountedResource(spi1, spi2, udpEncapSocket); + + // Pretend one SPI goes out of reference (releaseManagedResource -> userRelease) + spi1.userRelease(); + + // User called releaseManagedResource on udpEncap socket + udpEncapSocket.userRelease(); + + // User dies, and binder kills the rest + spi2.binderDied(); + transform.binderDied(); + + // Check resource states + assertResourceState(spi1, -1, 1, 2, 1, 1); + assertResourceState(spi2, -1, 1, 2, 1, 1); + assertResourceState(udpEncapSocket, -1, 1, 2, 1, 1); + assertResourceState(transform, -1, 1, 1, 1, 1); + } + + @Test + public void testSampleDualTransformEncapSocket() throws RemoteException { + RefcountedResource spi1 = getTestRefcountedResource(); + RefcountedResource spi2 = getTestRefcountedResource(); + RefcountedResource spi3 = getTestRefcountedResource(); + RefcountedResource spi4 = getTestRefcountedResource(); + RefcountedResource udpEncapSocket = getTestRefcountedResource(); + RefcountedResource transform1 = + getTestRefcountedResource(spi1, spi2, udpEncapSocket); + RefcountedResource transform2 = + getTestRefcountedResource(spi3, spi4, udpEncapSocket); + + // Pretend one SPIs goes out of reference (releaseManagedResource -> userRelease) + spi1.userRelease(); + + // User called releaseManagedResource on udpEncap socket and spi4 + udpEncapSocket.userRelease(); + spi4.userRelease(); + + // User dies, and binder kills the rest + spi2.binderDied(); + spi3.binderDied(); + transform2.binderDied(); + transform1.binderDied(); + + // Check resource states + assertResourceState(spi1, -1, 1, 2, 1, 1); + assertResourceState(spi2, -1, 1, 2, 1, 1); + assertResourceState(spi3, -1, 1, 2, 1, 1); + assertResourceState(spi4, -1, 1, 2, 1, 1); + assertResourceState(udpEncapSocket, -1, 1, 3, 1, 1); + assertResourceState(transform1, -1, 1, 1, 1, 1); + assertResourceState(transform2, -1, 1, 1, 1, 1); + } + + @Test + public void fuzzTest() throws RemoteException { + List> resources = new ArrayList<>(); + + // Build a tree of resources + for (int i = 0; i < 100; i++) { + // Choose a random number of children from the existing list + int numChildren = ThreadLocalRandom.current().nextInt(0, resources.size() + 1); + + // Build a (random) list of children + Set> children = new HashSet<>(); + for (int j = 0; j < numChildren; j++) { + int childIndex = ThreadLocalRandom.current().nextInt(0, resources.size()); + children.add(resources.get(childIndex)); + } + + RefcountedResource newRefcountedResource = + getTestRefcountedResource( + children.toArray(new RefcountedResource[children.size()])); + resources.add(newRefcountedResource); + } + + // Cleanup all resources in a random order + List> clonedResources = + new ArrayList<>(resources); // shallow copy + while (!clonedResources.isEmpty()) { + int index = ThreadLocalRandom.current().nextInt(0, clonedResources.size()); + RefcountedResource refcountedResource = clonedResources.get(index); + refcountedResource.userRelease(); + clonedResources.remove(index); + } + + // Verify all resources were cleaned up properly + for (RefcountedResource refcountedResource : resources) { + assertEquals(-1, refcountedResource.mRefCount); + } + } +} diff --git a/tests/unit/java/com/android/server/IpSecServiceTest.java b/tests/unit/java/com/android/server/IpSecServiceTest.java new file mode 100644 index 0000000000..6232423b4f --- /dev/null +++ b/tests/unit/java/com/android/server/IpSecServiceTest.java @@ -0,0 +1,670 @@ +/* + * 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; + +import static android.system.OsConstants.AF_INET; +import static android.system.OsConstants.EADDRINUSE; +import static android.system.OsConstants.IPPROTO_UDP; +import static android.system.OsConstants.SOCK_DGRAM; + +import static org.junit.Assert.assertEquals; +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.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.INetd; +import android.net.IpSecAlgorithm; +import android.net.IpSecConfig; +import android.net.IpSecManager; +import android.net.IpSecSpiResponse; +import android.net.IpSecUdpEncapResponse; +import android.os.Binder; +import android.os.ParcelFileDescriptor; +import android.os.Process; +import android.system.ErrnoException; +import android.system.Os; +import android.system.StructStat; +import android.util.Range; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import dalvik.system.SocketTagger; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatcher; + +import java.io.FileDescriptor; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; + +/** Unit tests for {@link IpSecService}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class IpSecServiceTest { + + private static final int DROID_SPI = 0xD1201D; + private static final int MAX_NUM_ENCAP_SOCKETS = 100; + private static final int MAX_NUM_SPIS = 100; + private static final int TEST_UDP_ENCAP_INVALID_PORT = 100; + private static final int TEST_UDP_ENCAP_PORT_OUT_RANGE = 100000; + + private static final InetAddress INADDR_ANY; + + private static final byte[] AEAD_KEY = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + 0x73, 0x61, 0x6C, 0x74 + }; + private static final byte[] CRYPT_KEY = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F + }; + private static final byte[] AUTH_KEY = { + 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, + 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F + }; + + private static final IpSecAlgorithm AUTH_ALGO = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, AUTH_KEY, AUTH_KEY.length * 4); + private static final IpSecAlgorithm CRYPT_ALGO = + new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + private static final IpSecAlgorithm AEAD_ALGO = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128); + + static { + try { + INADDR_ANY = InetAddress.getByAddress(new byte[] {0, 0, 0, 0}); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } + + Context mMockContext; + INetd mMockNetd; + IpSecService.IpSecServiceConfiguration mMockIpSecSrvConfig; + IpSecService mIpSecService; + + @Before + public void setUp() throws Exception { + mMockContext = mock(Context.class); + mMockNetd = mock(INetd.class); + mMockIpSecSrvConfig = mock(IpSecService.IpSecServiceConfiguration.class); + mIpSecService = new IpSecService(mMockContext, mMockIpSecSrvConfig); + + // Injecting mock netd + when(mMockIpSecSrvConfig.getNetdInstance()).thenReturn(mMockNetd); + } + + @Test + public void testIpSecServiceCreate() throws InterruptedException { + IpSecService ipSecSrv = IpSecService.create(mMockContext); + assertNotNull(ipSecSrv); + } + + @Test + public void testReleaseInvalidSecurityParameterIndex() throws Exception { + try { + mIpSecService.releaseSecurityParameterIndex(1); + fail("IllegalArgumentException not thrown"); + } catch (IllegalArgumentException e) { + } + } + + /** This function finds an available port */ + int findUnusedPort() throws Exception { + // Get an available port. + ServerSocket s = new ServerSocket(0); + int port = s.getLocalPort(); + s.close(); + return port; + } + + @Test + public void testOpenAndCloseUdpEncapsulationSocket() throws Exception { + int localport = -1; + IpSecUdpEncapResponse udpEncapResp = null; + + for (int i = 0; i < IpSecService.MAX_PORT_BIND_ATTEMPTS; i++) { + localport = findUnusedPort(); + + udpEncapResp = mIpSecService.openUdpEncapsulationSocket(localport, new Binder()); + assertNotNull(udpEncapResp); + if (udpEncapResp.status == IpSecManager.Status.OK) { + break; + } + + // Else retry to reduce possibility for port-bind failures. + } + + assertNotNull(udpEncapResp); + assertEquals(IpSecManager.Status.OK, udpEncapResp.status); + assertEquals(localport, udpEncapResp.port); + + mIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId); + udpEncapResp.fileDescriptor.close(); + + // Verify quota and RefcountedResource objects cleaned up + IpSecService.UserRecord userRecord = + mIpSecService.mUserResourceTracker.getUserRecord(Os.getuid()); + assertEquals(0, userRecord.mSocketQuotaTracker.mCurrent); + try { + userRecord.mEncapSocketRecords.getRefcountedResourceOrThrow(udpEncapResp.resourceId); + fail("Expected IllegalArgumentException on attempt to access deleted resource"); + } catch (IllegalArgumentException expected) { + + } + } + + @Test + public void testUdpEncapsulationSocketBinderDeath() throws Exception { + IpSecUdpEncapResponse udpEncapResp = + mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + + IpSecService.UserRecord userRecord = + mIpSecService.mUserResourceTracker.getUserRecord(Os.getuid()); + IpSecService.RefcountedResource refcountedRecord = + userRecord.mEncapSocketRecords.getRefcountedResourceOrThrow( + udpEncapResp.resourceId); + + refcountedRecord.binderDied(); + + // Verify quota and RefcountedResource objects cleaned up + assertEquals(0, userRecord.mSocketQuotaTracker.mCurrent); + try { + userRecord.mEncapSocketRecords.getRefcountedResourceOrThrow(udpEncapResp.resourceId); + fail("Expected IllegalArgumentException on attempt to access deleted resource"); + } catch (IllegalArgumentException expected) { + + } + } + + @Test + public void testOpenUdpEncapsulationSocketAfterClose() throws Exception { + IpSecUdpEncapResponse udpEncapResp = + mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + assertNotNull(udpEncapResp); + assertEquals(IpSecManager.Status.OK, udpEncapResp.status); + int localport = udpEncapResp.port; + + mIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId); + udpEncapResp.fileDescriptor.close(); + + /** Check if localport is available. */ + FileDescriptor newSocket = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + Os.bind(newSocket, INADDR_ANY, localport); + Os.close(newSocket); + } + + /** + * This function checks if the IpSecService holds the reserved port. If + * closeUdpEncapsulationSocket is not called, the socket cleanup should not be complete. + */ + @Test + public void testUdpEncapPortNotReleased() throws Exception { + IpSecUdpEncapResponse udpEncapResp = + mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + assertNotNull(udpEncapResp); + assertEquals(IpSecManager.Status.OK, udpEncapResp.status); + int localport = udpEncapResp.port; + + udpEncapResp.fileDescriptor.close(); + + FileDescriptor newSocket = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + try { + Os.bind(newSocket, INADDR_ANY, localport); + fail("ErrnoException not thrown"); + } catch (ErrnoException e) { + assertEquals(EADDRINUSE, e.errno); + } + mIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId); + } + + @Test + public void testOpenUdpEncapsulationSocketOnRandomPort() throws Exception { + IpSecUdpEncapResponse udpEncapResp = + mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + assertNotNull(udpEncapResp); + assertEquals(IpSecManager.Status.OK, udpEncapResp.status); + assertNotEquals(0, udpEncapResp.port); + mIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId); + udpEncapResp.fileDescriptor.close(); + } + + @Test + public void testOpenUdpEncapsulationSocketPortRange() throws Exception { + try { + mIpSecService.openUdpEncapsulationSocket(TEST_UDP_ENCAP_INVALID_PORT, new Binder()); + fail("IllegalArgumentException not thrown"); + } catch (IllegalArgumentException e) { + } + + try { + mIpSecService.openUdpEncapsulationSocket(TEST_UDP_ENCAP_PORT_OUT_RANGE, new Binder()); + fail("IllegalArgumentException not thrown"); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void testOpenUdpEncapsulationSocketTwice() throws Exception { + IpSecUdpEncapResponse udpEncapResp = + mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + assertNotNull(udpEncapResp); + assertEquals(IpSecManager.Status.OK, udpEncapResp.status); + int localport = udpEncapResp.port; + + IpSecUdpEncapResponse testUdpEncapResp = + mIpSecService.openUdpEncapsulationSocket(localport, new Binder()); + assertEquals(IpSecManager.Status.RESOURCE_UNAVAILABLE, testUdpEncapResp.status); + + mIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId); + udpEncapResp.fileDescriptor.close(); + } + + @Test + public void testCloseInvalidUdpEncapsulationSocket() throws Exception { + try { + mIpSecService.closeUdpEncapsulationSocket(1); + fail("IllegalArgumentException not thrown"); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void testValidateAlgorithmsAuth() { + // Validate that correct algorithm type succeeds + IpSecConfig config = new IpSecConfig(); + config.setAuthentication(AUTH_ALGO); + mIpSecService.validateAlgorithms(config); + + // Validate that incorrect algorithm types fails + for (IpSecAlgorithm algo : new IpSecAlgorithm[] {CRYPT_ALGO, AEAD_ALGO}) { + try { + config = new IpSecConfig(); + config.setAuthentication(algo); + mIpSecService.validateAlgorithms(config); + fail("Did not throw exception on invalid algorithm type"); + } catch (IllegalArgumentException expected) { + } + } + } + + @Test + public void testValidateAlgorithmsCrypt() { + // Validate that correct algorithm type succeeds + IpSecConfig config = new IpSecConfig(); + config.setEncryption(CRYPT_ALGO); + mIpSecService.validateAlgorithms(config); + + // Validate that incorrect algorithm types fails + for (IpSecAlgorithm algo : new IpSecAlgorithm[] {AUTH_ALGO, AEAD_ALGO}) { + try { + config = new IpSecConfig(); + config.setEncryption(algo); + mIpSecService.validateAlgorithms(config); + fail("Did not throw exception on invalid algorithm type"); + } catch (IllegalArgumentException expected) { + } + } + } + + @Test + public void testValidateAlgorithmsAead() { + // Validate that correct algorithm type succeeds + IpSecConfig config = new IpSecConfig(); + config.setAuthenticatedEncryption(AEAD_ALGO); + mIpSecService.validateAlgorithms(config); + + // Validate that incorrect algorithm types fails + for (IpSecAlgorithm algo : new IpSecAlgorithm[] {AUTH_ALGO, CRYPT_ALGO}) { + try { + config = new IpSecConfig(); + config.setAuthenticatedEncryption(algo); + mIpSecService.validateAlgorithms(config); + fail("Did not throw exception on invalid algorithm type"); + } catch (IllegalArgumentException expected) { + } + } + } + + @Test + public void testValidateAlgorithmsAuthCrypt() { + // Validate that correct algorithm type succeeds + IpSecConfig config = new IpSecConfig(); + config.setAuthentication(AUTH_ALGO); + config.setEncryption(CRYPT_ALGO); + mIpSecService.validateAlgorithms(config); + } + + @Test + public void testValidateAlgorithmsNoAlgorithms() { + IpSecConfig config = new IpSecConfig(); + try { + mIpSecService.validateAlgorithms(config); + fail("Expected exception; no algorithms specified"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testValidateAlgorithmsAeadWithAuth() { + IpSecConfig config = new IpSecConfig(); + config.setAuthenticatedEncryption(AEAD_ALGO); + config.setAuthentication(AUTH_ALGO); + try { + mIpSecService.validateAlgorithms(config); + fail("Expected exception; both AEAD and auth algorithm specified"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testValidateAlgorithmsAeadWithCrypt() { + IpSecConfig config = new IpSecConfig(); + config.setAuthenticatedEncryption(AEAD_ALGO); + config.setEncryption(CRYPT_ALGO); + try { + mIpSecService.validateAlgorithms(config); + fail("Expected exception; both AEAD and crypt algorithm specified"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testValidateAlgorithmsAeadWithAuthAndCrypt() { + IpSecConfig config = new IpSecConfig(); + config.setAuthenticatedEncryption(AEAD_ALGO); + config.setAuthentication(AUTH_ALGO); + config.setEncryption(CRYPT_ALGO); + try { + mIpSecService.validateAlgorithms(config); + fail("Expected exception; AEAD, auth and crypt algorithm specified"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testDeleteInvalidTransform() throws Exception { + try { + mIpSecService.deleteTransform(1); + fail("IllegalArgumentException not thrown"); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void testRemoveTransportModeTransform() throws Exception { + Socket socket = new Socket(); + socket.bind(null); + ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket); + mIpSecService.removeTransportModeTransforms(pfd); + + verify(mMockNetd).ipSecRemoveTransportModeTransform(pfd); + } + + @Test + public void testValidateIpAddresses() throws Exception { + String[] invalidAddresses = + new String[] {"www.google.com", "::", "2001::/64", "0.0.0.0", ""}; + for (String address : invalidAddresses) { + try { + IpSecSpiResponse spiResp = + mIpSecService.allocateSecurityParameterIndex( + address, DROID_SPI, new Binder()); + fail("Invalid address was passed through IpSecService validation: " + address); + } catch (IllegalArgumentException e) { + } catch (Exception e) { + fail( + "Invalid InetAddress was not caught in validation: " + + address + + ", Exception: " + + e); + } + } + } + + /** + * This function checks if the number of encap UDP socket that one UID can reserve has a + * reasonable limit. + */ + @Test + public void testSocketResourceTrackerLimitation() throws Exception { + List openUdpEncapSockets = new ArrayList(); + // Reserve sockets until it fails. + for (int i = 0; i < MAX_NUM_ENCAP_SOCKETS; i++) { + IpSecUdpEncapResponse newUdpEncapSocket = + mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + assertNotNull(newUdpEncapSocket); + if (IpSecManager.Status.OK != newUdpEncapSocket.status) { + break; + } + openUdpEncapSockets.add(newUdpEncapSocket); + } + // Assert that the total sockets quota has a reasonable limit. + assertTrue("No UDP encap socket was open", !openUdpEncapSockets.isEmpty()); + assertTrue( + "Number of open UDP encap sockets is out of bound", + openUdpEncapSockets.size() < MAX_NUM_ENCAP_SOCKETS); + + // Try to reserve one more UDP encapsulation socket, and should fail. + IpSecUdpEncapResponse extraUdpEncapSocket = + mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + assertNotNull(extraUdpEncapSocket); + assertEquals(IpSecManager.Status.RESOURCE_UNAVAILABLE, extraUdpEncapSocket.status); + + // Close one of the open UDP encapsulation sockets. + mIpSecService.closeUdpEncapsulationSocket(openUdpEncapSockets.get(0).resourceId); + openUdpEncapSockets.get(0).fileDescriptor.close(); + openUdpEncapSockets.remove(0); + + // Try to reserve one more UDP encapsulation socket, and should be successful. + extraUdpEncapSocket = mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + assertNotNull(extraUdpEncapSocket); + assertEquals(IpSecManager.Status.OK, extraUdpEncapSocket.status); + openUdpEncapSockets.add(extraUdpEncapSocket); + + // Close open UDP sockets. + for (IpSecUdpEncapResponse openSocket : openUdpEncapSockets) { + mIpSecService.closeUdpEncapsulationSocket(openSocket.resourceId); + openSocket.fileDescriptor.close(); + } + } + + /** + * This function checks if the number of SPI that one UID can reserve has a reasonable limit. + * This test does not test for both address families or duplicate SPIs because resource tracking + * code does not depend on them. + */ + @Test + public void testSpiResourceTrackerLimitation() throws Exception { + List reservedSpis = new ArrayList(); + // Return the same SPI for all SPI allocation since IpSecService only + // tracks the resource ID. + when(mMockNetd.ipSecAllocateSpi( + anyInt(), + anyString(), + eq(InetAddress.getLoopbackAddress().getHostAddress()), + anyInt())) + .thenReturn(DROID_SPI); + // Reserve spis until it fails. + for (int i = 0; i < MAX_NUM_SPIS; i++) { + IpSecSpiResponse newSpi = + mIpSecService.allocateSecurityParameterIndex( + InetAddress.getLoopbackAddress().getHostAddress(), + DROID_SPI + i, + new Binder()); + assertNotNull(newSpi); + if (IpSecManager.Status.OK != newSpi.status) { + break; + } + reservedSpis.add(newSpi); + } + // Assert that the SPI quota has a reasonable limit. + assertTrue(reservedSpis.size() > 0 && reservedSpis.size() < MAX_NUM_SPIS); + + // Try to reserve one more SPI, and should fail. + IpSecSpiResponse extraSpi = + mIpSecService.allocateSecurityParameterIndex( + InetAddress.getLoopbackAddress().getHostAddress(), + DROID_SPI + MAX_NUM_SPIS, + new Binder()); + assertNotNull(extraSpi); + assertEquals(IpSecManager.Status.RESOURCE_UNAVAILABLE, extraSpi.status); + + // Release one reserved spi. + mIpSecService.releaseSecurityParameterIndex(reservedSpis.get(0).resourceId); + reservedSpis.remove(0); + + // Should successfully reserve one more spi. + extraSpi = + mIpSecService.allocateSecurityParameterIndex( + InetAddress.getLoopbackAddress().getHostAddress(), + DROID_SPI + MAX_NUM_SPIS, + new Binder()); + assertNotNull(extraSpi); + assertEquals(IpSecManager.Status.OK, extraSpi.status); + + // Release reserved SPIs. + for (IpSecSpiResponse spiResp : reservedSpis) { + mIpSecService.releaseSecurityParameterIndex(spiResp.resourceId); + } + } + + @Test + public void testUidFdtagger() throws Exception { + SocketTagger actualSocketTagger = SocketTagger.get(); + + try { + FileDescriptor sockFd = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + + // Has to be done after socket creation because BlockGuardOS calls tag on new sockets + SocketTagger mockSocketTagger = mock(SocketTagger.class); + SocketTagger.set(mockSocketTagger); + + mIpSecService.mUidFdTagger.tag(sockFd, Process.LAST_APPLICATION_UID); + verify(mockSocketTagger).tag(eq(sockFd)); + } finally { + SocketTagger.set(actualSocketTagger); + } + } + + /** + * Checks if two file descriptors point to the same file. + * + *

According to stat.h documentation, the correct way to check for equivalent or duplicated + * file descriptors is to check their inode and device. These two entries uniquely identify any + * file. + */ + private boolean fileDescriptorsEqual(FileDescriptor fd1, FileDescriptor fd2) { + try { + StructStat fd1Stat = Os.fstat(fd1); + StructStat fd2Stat = Os.fstat(fd2); + + return fd1Stat.st_ino == fd2Stat.st_ino && fd1Stat.st_dev == fd2Stat.st_dev; + } catch (ErrnoException e) { + return false; + } + } + + @Test + public void testOpenUdpEncapSocketTagsSocket() throws Exception { + IpSecService.UidFdTagger mockTagger = mock(IpSecService.UidFdTagger.class); + IpSecService testIpSecService = new IpSecService( + mMockContext, mMockIpSecSrvConfig, mockTagger); + + IpSecUdpEncapResponse udpEncapResp = + testIpSecService.openUdpEncapsulationSocket(0, new Binder()); + assertNotNull(udpEncapResp); + assertEquals(IpSecManager.Status.OK, udpEncapResp.status); + + FileDescriptor sockFd = udpEncapResp.fileDescriptor.getFileDescriptor(); + ArgumentMatcher fdMatcher = + (argFd) -> { + return fileDescriptorsEqual(sockFd, argFd); + }; + verify(mockTagger).tag(argThat(fdMatcher), eq(Os.getuid())); + + testIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId); + udpEncapResp.fileDescriptor.close(); + } + + @Test + public void testOpenUdpEncapsulationSocketCallsSetEncapSocketOwner() throws Exception { + IpSecUdpEncapResponse udpEncapResp = + mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + + FileDescriptor sockFd = udpEncapResp.fileDescriptor.getFileDescriptor(); + ArgumentMatcher fdMatcher = (arg) -> { + try { + StructStat sockStat = Os.fstat(sockFd); + StructStat argStat = Os.fstat(arg.getFileDescriptor()); + + return sockStat.st_ino == argStat.st_ino + && sockStat.st_dev == argStat.st_dev; + } catch (ErrnoException e) { + return false; + } + }; + + verify(mMockNetd).ipSecSetEncapSocketOwner(argThat(fdMatcher), eq(Os.getuid())); + mIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId); + } + + @Test + public void testReserveNetId() { + final Range netIdRange = ConnectivityManager.getIpSecNetIdRange(); + for (int netId = netIdRange.getLower(); netId <= netIdRange.getUpper(); netId++) { + assertEquals(netId, mIpSecService.reserveNetId()); + } + + // Check that resource exhaustion triggers an exception + try { + mIpSecService.reserveNetId(); + fail("Did not throw error for all netIds reserved"); + } catch (IllegalStateException expected) { + } + + // Now release one and try again + int releasedNetId = + netIdRange.getLower() + (netIdRange.getUpper() - netIdRange.getLower()) / 2; + mIpSecService.releaseNetId(releasedNetId); + assertEquals(releasedNetId, mIpSecService.reserveNetId()); + } +} diff --git a/tests/unit/java/com/android/server/LegacyTypeTrackerTest.kt b/tests/unit/java/com/android/server/LegacyTypeTrackerTest.kt new file mode 100644 index 0000000000..5ec111954f --- /dev/null +++ b/tests/unit/java/com/android/server/LegacyTypeTrackerTest.kt @@ -0,0 +1,197 @@ +/* + * 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. + */ + +// Don't warn about deprecated types anywhere in this test, because LegacyTypeTracker's very reason +// for existence is to power deprecated APIs. The annotation has to apply to the whole file because +// otherwise warnings will be generated by the imports of deprecated constants like TYPE_xxx. +@file:Suppress("DEPRECATION") + +package com.android.server + +import android.content.Context +import android.content.pm.PackageManager +import android.content.pm.PackageManager.FEATURE_WIFI +import android.content.pm.PackageManager.FEATURE_WIFI_DIRECT +import android.net.ConnectivityManager.TYPE_ETHERNET +import android.net.ConnectivityManager.TYPE_MOBILE +import android.net.ConnectivityManager.TYPE_MOBILE_CBS +import android.net.ConnectivityManager.TYPE_MOBILE_DUN +import android.net.ConnectivityManager.TYPE_MOBILE_EMERGENCY +import android.net.ConnectivityManager.TYPE_MOBILE_FOTA +import android.net.ConnectivityManager.TYPE_MOBILE_HIPRI +import android.net.ConnectivityManager.TYPE_MOBILE_IA +import android.net.ConnectivityManager.TYPE_MOBILE_IMS +import android.net.ConnectivityManager.TYPE_MOBILE_MMS +import android.net.ConnectivityManager.TYPE_MOBILE_SUPL +import android.net.ConnectivityManager.TYPE_VPN +import android.net.ConnectivityManager.TYPE_WIFI +import android.net.ConnectivityManager.TYPE_WIFI_P2P +import android.net.ConnectivityManager.TYPE_WIMAX +import android.net.EthernetManager +import android.net.NetworkInfo.DetailedState.CONNECTED +import android.net.NetworkInfo.DetailedState.DISCONNECTED +import android.telephony.TelephonyManager +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.server.ConnectivityService.LegacyTypeTracker +import com.android.server.connectivity.NetworkAgentInfo +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertSame +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify + +const val UNSUPPORTED_TYPE = TYPE_WIMAX + +@RunWith(AndroidJUnit4::class) +@SmallTest +class LegacyTypeTrackerTest { + private val supportedTypes = arrayOf(TYPE_WIFI, TYPE_WIFI_P2P, TYPE_ETHERNET, TYPE_MOBILE, + TYPE_MOBILE_SUPL, TYPE_MOBILE_MMS, TYPE_MOBILE_SUPL, TYPE_MOBILE_DUN, TYPE_MOBILE_HIPRI, + TYPE_MOBILE_FOTA, TYPE_MOBILE_IMS, TYPE_MOBILE_CBS, TYPE_MOBILE_IA, + TYPE_MOBILE_EMERGENCY, TYPE_VPN) + + private val mMockService = mock(ConnectivityService::class.java).apply { + doReturn(false).`when`(this).isDefaultNetwork(any()) + } + private val mPm = mock(PackageManager::class.java) + private val mContext = mock(Context::class.java).apply { + doReturn(true).`when`(mPm).hasSystemFeature(FEATURE_WIFI) + doReturn(true).`when`(mPm).hasSystemFeature(FEATURE_WIFI_DIRECT) + doReturn(mPm).`when`(this).packageManager + doReturn(mock(EthernetManager::class.java)).`when`(this).getSystemService( + Context.ETHERNET_SERVICE) + } + private val mTm = mock(TelephonyManager::class.java).apply { + doReturn(true).`when`(this).isDataCapable + } + + private fun makeTracker() = LegacyTypeTracker(mMockService).apply { + loadSupportedTypes(mContext, mTm) + } + + @Test + fun testSupportedTypes() { + val tracker = makeTracker() + supportedTypes.forEach { + assertTrue(tracker.isTypeSupported(it)) + } + assertFalse(tracker.isTypeSupported(UNSUPPORTED_TYPE)) + } + + @Test + fun testSupportedTypes_NoEthernet() { + doReturn(null).`when`(mContext).getSystemService(Context.ETHERNET_SERVICE) + assertFalse(makeTracker().isTypeSupported(TYPE_ETHERNET)) + } + + @Test + fun testSupportedTypes_NoTelephony() { + doReturn(false).`when`(mTm).isDataCapable + val tracker = makeTracker() + val nonMobileTypes = arrayOf(TYPE_WIFI, TYPE_WIFI_P2P, TYPE_ETHERNET, TYPE_VPN) + nonMobileTypes.forEach { + assertTrue(tracker.isTypeSupported(it)) + } + supportedTypes.toSet().minus(nonMobileTypes).forEach { + assertFalse(tracker.isTypeSupported(it)) + } + } + + @Test + fun testSupportedTypes_NoWifiDirect() { + doReturn(false).`when`(mPm).hasSystemFeature(FEATURE_WIFI_DIRECT) + val tracker = makeTracker() + assertFalse(tracker.isTypeSupported(TYPE_WIFI_P2P)) + supportedTypes.toSet().minus(TYPE_WIFI_P2P).forEach { + assertTrue(tracker.isTypeSupported(it)) + } + } + + @Test + fun testSupl() { + val tracker = makeTracker() + val mobileNai = mock(NetworkAgentInfo::class.java) + tracker.add(TYPE_MOBILE, mobileNai) + verify(mMockService).sendLegacyNetworkBroadcast(mobileNai, CONNECTED, TYPE_MOBILE) + reset(mMockService) + tracker.add(TYPE_MOBILE_SUPL, mobileNai) + verify(mMockService).sendLegacyNetworkBroadcast(mobileNai, CONNECTED, TYPE_MOBILE_SUPL) + reset(mMockService) + tracker.remove(TYPE_MOBILE_SUPL, mobileNai, false /* wasDefault */) + verify(mMockService).sendLegacyNetworkBroadcast(mobileNai, DISCONNECTED, TYPE_MOBILE_SUPL) + reset(mMockService) + tracker.add(TYPE_MOBILE_SUPL, mobileNai) + verify(mMockService).sendLegacyNetworkBroadcast(mobileNai, CONNECTED, TYPE_MOBILE_SUPL) + reset(mMockService) + tracker.remove(mobileNai, false) + verify(mMockService).sendLegacyNetworkBroadcast(mobileNai, DISCONNECTED, TYPE_MOBILE_SUPL) + verify(mMockService).sendLegacyNetworkBroadcast(mobileNai, DISCONNECTED, TYPE_MOBILE) + } + + @Test + fun testAddNetwork() { + val tracker = makeTracker() + val mobileNai = mock(NetworkAgentInfo::class.java) + val wifiNai = mock(NetworkAgentInfo::class.java) + tracker.add(TYPE_MOBILE, mobileNai) + tracker.add(TYPE_WIFI, wifiNai) + assertSame(tracker.getNetworkForType(TYPE_MOBILE), mobileNai) + assertSame(tracker.getNetworkForType(TYPE_WIFI), wifiNai) + // Make sure adding a second NAI does not change the results. + val secondMobileNai = mock(NetworkAgentInfo::class.java) + tracker.add(TYPE_MOBILE, secondMobileNai) + assertSame(tracker.getNetworkForType(TYPE_MOBILE), mobileNai) + assertSame(tracker.getNetworkForType(TYPE_WIFI), wifiNai) + // Make sure removing a network that wasn't added for this type is a no-op. + tracker.remove(TYPE_MOBILE, wifiNai, false /* wasDefault */) + assertSame(tracker.getNetworkForType(TYPE_MOBILE), mobileNai) + assertSame(tracker.getNetworkForType(TYPE_WIFI), wifiNai) + // Remove the top network for mobile and make sure the second one becomes the network + // of record for this type. + tracker.remove(TYPE_MOBILE, mobileNai, false /* wasDefault */) + assertSame(tracker.getNetworkForType(TYPE_MOBILE), secondMobileNai) + assertSame(tracker.getNetworkForType(TYPE_WIFI), wifiNai) + // Make sure adding a network for an unsupported type does not register it. + tracker.add(UNSUPPORTED_TYPE, mobileNai) + assertNull(tracker.getNetworkForType(UNSUPPORTED_TYPE)) + } + + @Test + fun testBroadcastOnDisconnect() { + val tracker = makeTracker() + val mobileNai1 = mock(NetworkAgentInfo::class.java) + val mobileNai2 = mock(NetworkAgentInfo::class.java) + doReturn(false).`when`(mMockService).isDefaultNetwork(mobileNai1) + tracker.add(TYPE_MOBILE, mobileNai1) + verify(mMockService).sendLegacyNetworkBroadcast(mobileNai1, CONNECTED, TYPE_MOBILE) + reset(mMockService) + doReturn(false).`when`(mMockService).isDefaultNetwork(mobileNai2) + tracker.add(TYPE_MOBILE, mobileNai2) + verify(mMockService, never()).sendLegacyNetworkBroadcast(any(), any(), anyInt()) + tracker.remove(TYPE_MOBILE, mobileNai1, false /* wasDefault */) + verify(mMockService).sendLegacyNetworkBroadcast(mobileNai1, DISCONNECTED, TYPE_MOBILE) + verify(mMockService).sendLegacyNetworkBroadcast(mobileNai2, CONNECTED, TYPE_MOBILE) + } +} diff --git a/tests/unit/java/com/android/server/NetIdManagerTest.kt b/tests/unit/java/com/android/server/NetIdManagerTest.kt new file mode 100644 index 0000000000..6f5e740d34 --- /dev/null +++ b/tests/unit/java/com/android/server/NetIdManagerTest.kt @@ -0,0 +1,53 @@ +/* + * 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 com.android.server + +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.server.NetIdManager.MIN_NET_ID +import com.android.testutils.assertThrows +import com.android.testutils.ExceptionUtils.ThrowingRunnable +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.test.assertEquals + +@RunWith(AndroidJUnit4::class) +@SmallTest +class NetIdManagerTest { + @Test + fun testReserveReleaseNetId() { + val manager = NetIdManager(MIN_NET_ID + 4) + assertEquals(MIN_NET_ID, manager.reserveNetId()) + assertEquals(MIN_NET_ID + 1, manager.reserveNetId()) + assertEquals(MIN_NET_ID + 2, manager.reserveNetId()) + assertEquals(MIN_NET_ID + 3, manager.reserveNetId()) + + manager.releaseNetId(MIN_NET_ID + 1) + manager.releaseNetId(MIN_NET_ID + 3) + // IDs only loop once there is no higher ID available + assertEquals(MIN_NET_ID + 4, manager.reserveNetId()) + assertEquals(MIN_NET_ID + 1, manager.reserveNetId()) + assertEquals(MIN_NET_ID + 3, manager.reserveNetId()) + assertThrows(IllegalStateException::class.java, ThrowingRunnable { manager.reserveNetId() }) + manager.releaseNetId(MIN_NET_ID + 5) + // Still no ID available: MIN_NET_ID + 5 was not reserved + assertThrows(IllegalStateException::class.java, ThrowingRunnable { manager.reserveNetId() }) + manager.releaseNetId(MIN_NET_ID + 2) + // Throwing an exception still leaves the manager in a working state + assertEquals(MIN_NET_ID + 2, manager.reserveNetId()) + } +} \ No newline at end of file diff --git a/tests/unit/java/com/android/server/NetworkManagementServiceTest.java b/tests/unit/java/com/android/server/NetworkManagementServiceTest.java new file mode 100644 index 0000000000..13516d75a5 --- /dev/null +++ b/tests/unit/java/com/android/server/NetworkManagementServiceTest.java @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2012 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; + +import static android.util.DebugUtils.valueToString; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import android.annotation.NonNull; +import android.content.Context; +import android.net.INetd; +import android.net.INetdUnsolicitedEventListener; +import android.net.LinkAddress; +import android.net.NetworkPolicyManager; +import android.os.BatteryStats; +import android.os.Binder; +import android.os.IBinder; +import android.os.Process; +import android.os.RemoteException; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.ArrayMap; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.app.IBatteryStats; +import com.android.server.NetworkManagementService.Dependencies; +import com.android.server.net.BaseNetworkObserver; + +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.util.function.BiFunction; + +/** + * Tests for {@link NetworkManagementService}. + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkManagementServiceTest { + private NetworkManagementService mNMService; + @Mock private Context mContext; + @Mock private IBatteryStats.Stub mBatteryStatsService; + @Mock private INetd.Stub mNetdService; + + private static final int TEST_UID = 111; + + @NonNull + @Captor + private ArgumentCaptor mUnsolListenerCaptor; + + private final MockDependencies mDeps = new MockDependencies(); + + private final class MockDependencies extends Dependencies { + @Override + public IBinder getService(String name) { + switch (name) { + case BatteryStats.SERVICE_NAME: + return mBatteryStatsService; + default: + throw new UnsupportedOperationException("Unknown service " + name); + } + } + + @Override + public void registerLocalService(NetworkManagementInternal nmi) { + } + + @Override + public INetd getNetd() { + return mNetdService; + } + + @Override + public int getCallingUid() { + return Process.SYSTEM_UID; + } + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + doNothing().when(mNetdService) + .registerUnsolicitedEventListener(mUnsolListenerCaptor.capture()); + // Start the service and wait until it connects to our socket. + mNMService = NetworkManagementService.create(mContext, mDeps); + } + + @After + public void tearDown() throws Exception { + mNMService.shutdown(); + } + + private static T expectSoon(T mock) { + return verify(mock, timeout(200)); + } + + /** + * Tests that network observers work properly. + */ + @Test + public void testNetworkObservers() throws Exception { + BaseNetworkObserver observer = mock(BaseNetworkObserver.class); + doReturn(new Binder()).when(observer).asBinder(); // Used by registerObserver. + mNMService.registerObserver(observer); + + // Forget everything that happened to the mock so far, so we can explicitly verify + // everything that happens and does not happen to it from now on. + + INetdUnsolicitedEventListener unsolListener = mUnsolListenerCaptor.getValue(); + reset(observer); + // Now call unsolListener methods and ensure that the observer methods are + // called. After every method we expect a callback soon after; to ensure that + // invalid messages don't cause any callbacks, we call verifyNoMoreInteractions at the end. + + /** + * Interface changes. + */ + unsolListener.onInterfaceAdded("rmnet12"); + expectSoon(observer).interfaceAdded("rmnet12"); + + unsolListener.onInterfaceRemoved("eth1"); + expectSoon(observer).interfaceRemoved("eth1"); + + unsolListener.onInterfaceChanged("clat4", true); + expectSoon(observer).interfaceStatusChanged("clat4", true); + + unsolListener.onInterfaceLinkStateChanged("rmnet0", false); + expectSoon(observer).interfaceLinkStateChanged("rmnet0", false); + + /** + * Bandwidth control events. + */ + unsolListener.onQuotaLimitReached("data", "rmnet_usb0"); + expectSoon(observer).limitReached("data", "rmnet_usb0"); + + /** + * Interface class activity. + */ + unsolListener.onInterfaceClassActivityChanged(true, 1, 1234, TEST_UID); + expectSoon(observer).interfaceClassDataActivityChanged(1, true, 1234, TEST_UID); + + unsolListener.onInterfaceClassActivityChanged(false, 9, 5678, TEST_UID); + expectSoon(observer).interfaceClassDataActivityChanged(9, false, 5678, TEST_UID); + + unsolListener.onInterfaceClassActivityChanged(false, 9, 4321, TEST_UID); + expectSoon(observer).interfaceClassDataActivityChanged(9, false, 4321, TEST_UID); + + /** + * IP address changes. + */ + unsolListener.onInterfaceAddressUpdated("fe80::1/64", "wlan0", 128, 253); + expectSoon(observer).addressUpdated("wlan0", new LinkAddress("fe80::1/64", 128, 253)); + + unsolListener.onInterfaceAddressRemoved("fe80::1/64", "wlan0", 128, 253); + expectSoon(observer).addressRemoved("wlan0", new LinkAddress("fe80::1/64", 128, 253)); + + unsolListener.onInterfaceAddressRemoved("2001:db8::1/64", "wlan0", 1, 0); + expectSoon(observer).addressRemoved("wlan0", new LinkAddress("2001:db8::1/64", 1, 0)); + + /** + * DNS information broadcasts. + */ + unsolListener.onInterfaceDnsServerInfo("rmnet_usb0", 3600, new String[]{"2001:db8::1"}); + expectSoon(observer).interfaceDnsServerInfo("rmnet_usb0", 3600, + new String[]{"2001:db8::1"}); + + unsolListener.onInterfaceDnsServerInfo("wlan0", 14400, + new String[]{"2001:db8::1", "2001:db8::2"}); + expectSoon(observer).interfaceDnsServerInfo("wlan0", 14400, + new String[]{"2001:db8::1", "2001:db8::2"}); + + // We don't check for negative lifetimes, only for parse errors. + unsolListener.onInterfaceDnsServerInfo("wlan0", -3600, new String[]{"::1"}); + expectSoon(observer).interfaceDnsServerInfo("wlan0", -3600, + new String[]{"::1"}); + + // No syntax checking on the addresses. + unsolListener.onInterfaceDnsServerInfo("wlan0", 600, + new String[]{"", "::", "", "foo", "::1"}); + expectSoon(observer).interfaceDnsServerInfo("wlan0", 600, + new String[]{"", "::", "", "foo", "::1"}); + + // Make sure nothing else was called. + verifyNoMoreInteractions(observer); + } + + @Test + public void testFirewallEnabled() { + mNMService.setFirewallEnabled(true); + assertTrue(mNMService.isFirewallEnabled()); + + mNMService.setFirewallEnabled(false); + assertFalse(mNMService.isFirewallEnabled()); + } + + @Test + public void testNetworkRestrictedDefault() { + assertFalse(mNMService.isNetworkRestricted(TEST_UID)); + } + + @Test + public void testMeteredNetworkRestrictions() throws RemoteException { + // Make sure the mocked netd method returns true. + doReturn(true).when(mNetdService).bandwidthEnableDataSaver(anyBoolean()); + + // Restrict usage of mobile data in background + mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, true); + assertTrue("Should be true since mobile data usage is restricted", + mNMService.isNetworkRestricted(TEST_UID)); + + mNMService.setDataSaverModeEnabled(true); + verify(mNetdService).bandwidthEnableDataSaver(true); + + mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, false); + assertTrue("Should be true since data saver is on and the uid is not allowlisted", + mNMService.isNetworkRestricted(TEST_UID)); + + mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, true); + assertFalse("Should be false since data saver is on and the uid is allowlisted", + mNMService.isNetworkRestricted(TEST_UID)); + + // remove uid from allowlist and turn datasaver off again + mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, false); + mNMService.setDataSaverModeEnabled(false); + verify(mNetdService).bandwidthEnableDataSaver(false); + assertFalse("Network should not be restricted when data saver is off", + mNMService.isNetworkRestricted(TEST_UID)); + } + + @Test + public void testFirewallChains() { + final ArrayMap> expected = new ArrayMap<>(); + // Dozable chain + final ArrayMap isRestrictedForDozable = new ArrayMap<>(); + isRestrictedForDozable.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true); + isRestrictedForDozable.put(INetd.FIREWALL_RULE_ALLOW, false); + isRestrictedForDozable.put(INetd.FIREWALL_RULE_DENY, true); + expected.put(INetd.FIREWALL_CHAIN_DOZABLE, isRestrictedForDozable); + // Powersaver chain + final ArrayMap isRestrictedForPowerSave = new ArrayMap<>(); + isRestrictedForPowerSave.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true); + isRestrictedForPowerSave.put(INetd.FIREWALL_RULE_ALLOW, false); + isRestrictedForPowerSave.put(INetd.FIREWALL_RULE_DENY, true); + expected.put(INetd.FIREWALL_CHAIN_POWERSAVE, isRestrictedForPowerSave); + // Standby chain + final ArrayMap isRestrictedForStandby = new ArrayMap<>(); + isRestrictedForStandby.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, false); + isRestrictedForStandby.put(INetd.FIREWALL_RULE_ALLOW, false); + isRestrictedForStandby.put(INetd.FIREWALL_RULE_DENY, true); + expected.put(INetd.FIREWALL_CHAIN_STANDBY, isRestrictedForStandby); + // Restricted mode chain + final ArrayMap isRestrictedForRestrictedMode = new ArrayMap<>(); + isRestrictedForRestrictedMode.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true); + isRestrictedForRestrictedMode.put(INetd.FIREWALL_RULE_ALLOW, false); + isRestrictedForRestrictedMode.put(INetd.FIREWALL_RULE_DENY, true); + expected.put(INetd.FIREWALL_CHAIN_RESTRICTED, isRestrictedForRestrictedMode); + + final int[] chains = { + INetd.FIREWALL_CHAIN_STANDBY, + INetd.FIREWALL_CHAIN_POWERSAVE, + INetd.FIREWALL_CHAIN_DOZABLE, + INetd.FIREWALL_CHAIN_RESTRICTED + }; + final int[] states = { + INetd.FIREWALL_RULE_ALLOW, + INetd.FIREWALL_RULE_DENY, + NetworkPolicyManager.FIREWALL_RULE_DEFAULT + }; + BiFunction errorMsg = (chain, state) -> { + return String.format("Unexpected value for chain: %s and state: %s", + valueToString(INetd.class, "FIREWALL_CHAIN_", chain), + valueToString(INetd.class, "FIREWALL_RULE_", state)); + }; + for (int chain : chains) { + final ArrayMap expectedValues = expected.get(chain); + mNMService.setFirewallChainEnabled(chain, true); + for (int state : states) { + mNMService.setFirewallUidRule(chain, TEST_UID, state); + assertEquals(errorMsg.apply(chain, state), + expectedValues.get(state), mNMService.isNetworkRestricted(TEST_UID)); + } + mNMService.setFirewallChainEnabled(chain, false); + } + } +} diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java new file mode 100644 index 0000000000..a90fa6882c --- /dev/null +++ b/tests/unit/java/com/android/server/NsdServiceTest.java @@ -0,0 +1,194 @@ +/* + * 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; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ContentResolver; +import android.content.Context; +import android.net.nsd.NsdManager; +import android.net.nsd.NsdServiceInfo; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.NsdService.DaemonConnection; +import com.android.server.NsdService.DaemonConnectionSupplier; +import com.android.server.NsdService.NativeCallbackReceiver; + +import org.junit.After; +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; + +// TODOs: +// - test client can send requests and receive replies +// - test NSD_ON ENABLE/DISABLED listening +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NsdServiceTest { + + static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD; + + long mTimeoutMs = 100; // non-final so that tests can adjust the value. + + @Mock Context mContext; + @Mock ContentResolver mResolver; + @Mock NsdService.NsdSettings mSettings; + @Mock DaemonConnection mDaemon; + NativeCallbackReceiver mDaemonCallback; + HandlerThread mThread; + TestHandler mHandler; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mThread = new HandlerThread("mock-service-handler"); + mThread.start(); + mHandler = new TestHandler(mThread.getLooper()); + when(mContext.getContentResolver()).thenReturn(mResolver); + } + + @After + public void tearDown() throws Exception { + if (mThread != null) { + mThread.quit(); + mThread = null; + } + } + + @Test + public void testClientsCanConnectAndDisconnect() { + when(mSettings.isEnabled()).thenReturn(true); + + NsdService service = makeService(); + + NsdManager client1 = connectClient(service); + verify(mDaemon, timeout(100).times(1)).start(); + + NsdManager client2 = connectClient(service); + + client1.disconnect(); + client2.disconnect(); + + verify(mDaemon, timeout(mTimeoutMs).times(1)).stop(); + + client1.disconnect(); + client2.disconnect(); + } + + @Test + public void testClientRequestsAreGCedAtDisconnection() { + when(mSettings.isEnabled()).thenReturn(true); + when(mDaemon.execute(any())).thenReturn(true); + + NsdService service = makeService(); + NsdManager client = connectClient(service); + + verify(mDaemon, timeout(100).times(1)).start(); + + NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type"); + request.setPort(2201); + + // Client registration request + NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class); + client.registerService(request, PROTOCOL, listener1); + verifyDaemonCommand("register 2 a_name a_type 2201"); + + // Client discovery request + NsdManager.DiscoveryListener listener2 = mock(NsdManager.DiscoveryListener.class); + client.discoverServices("a_type", PROTOCOL, listener2); + verifyDaemonCommand("discover 3 a_type"); + + // Client resolve request + NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class); + client.resolveService(request, listener3); + verifyDaemonCommand("resolve 4 a_name a_type local."); + + // Client disconnects + client.disconnect(); + verify(mDaemon, timeout(mTimeoutMs).times(1)).stop(); + + // checks that request are cleaned + verifyDaemonCommands("stop-register 2", "stop-discover 3", "stop-resolve 4"); + + client.disconnect(); + } + + NsdService makeService() { + DaemonConnectionSupplier supplier = (callback) -> { + mDaemonCallback = callback; + return mDaemon; + }; + NsdService service = new NsdService(mContext, mSettings, mHandler, supplier); + verify(mDaemon, never()).execute(any(String.class)); + return service; + } + + NsdManager connectClient(NsdService service) { + return new NsdManager(mContext, service); + } + + void verifyDaemonCommands(String... wants) { + verifyDaemonCommand(String.join(" ", wants), wants.length); + } + + void verifyDaemonCommand(String want) { + verifyDaemonCommand(want, 1); + } + + void verifyDaemonCommand(String want, int n) { + ArgumentCaptor argumentsCaptor = ArgumentCaptor.forClass(Object.class); + verify(mDaemon, timeout(mTimeoutMs).times(n)).execute(argumentsCaptor.capture()); + String got = ""; + for (Object o : argumentsCaptor.getAllValues()) { + got += o + " "; + } + assertEquals(want, got.trim()); + // rearm deamon for next command verification + reset(mDaemon); + when(mDaemon.execute(any())).thenReturn(true); + } + + public static class TestHandler extends Handler { + public Message lastMessage; + + TestHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + lastMessage = obtainMessage(); + lastMessage.copyFrom(msg); + } + } +} diff --git a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java new file mode 100644 index 0000000000..0ffeec98cf --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java @@ -0,0 +1,433 @@ +/* + * 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 com.android.server.connectivity; + +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_DEFAULT_MODE; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_SPECIFIER; +import static android.net.NetworkCapabilities.MAX_TRANSPORT; +import static android.net.NetworkCapabilities.MIN_TRANSPORT; +import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_FAILURE; +import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_SUCCESS; + +import static com.android.testutils.MiscAsserts.assertContainsExactly; +import static com.android.testutils.MiscAsserts.assertContainsStringsExactly; +import static com.android.testutils.MiscAsserts.assertFieldCountEquals; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.annotation.NonNull; +import android.content.Context; +import android.net.ConnectivitySettingsManager; +import android.net.IDnsResolver; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.ResolverOptionsParcel; +import android.net.ResolverParamsParcel; +import android.net.RouteInfo; +import android.net.shared.PrivateDnsConfig; +import android.provider.Settings; +import android.test.mock.MockContentResolver; +import android.util.SparseArray; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.MessageUtils; +import com.android.internal.util.test.FakeSettingsProvider; + +import libcore.net.InetAddressUtils; + +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 java.net.InetAddress; +import java.util.Arrays; + +/** + * Tests for {@link DnsManager}. + * + * Build, install and run with: + * runtest frameworks-net -c com.android.server.connectivity.DnsManagerTest + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class DnsManagerTest { + static final String TEST_IFACENAME = "test_wlan0"; + static final int TEST_NETID = 100; + static final int TEST_NETID_ALTERNATE = 101; + static final int TEST_NETID_UNTRACKED = 102; + static final int TEST_DEFAULT_SAMPLE_VALIDITY_SECONDS = 1800; + static final int TEST_DEFAULT_SUCCESS_THRESHOLD_PERCENT = 25; + static final int TEST_DEFAULT_MIN_SAMPLES = 8; + static final int TEST_DEFAULT_MAX_SAMPLES = 64; + static final int[] TEST_TRANSPORT_TYPES = {TRANSPORT_WIFI, TRANSPORT_VPN}; + + DnsManager mDnsManager; + MockContentResolver mContentResolver; + + @Mock Context mCtx; + @Mock IDnsResolver mMockDnsResolver; + + private void assertResolverOptionsEquals( + @NonNull ResolverOptionsParcel actual, + @NonNull ResolverOptionsParcel expected) { + assertEquals(actual.hosts, expected.hosts); + assertEquals(actual.tcMode, expected.tcMode); + assertEquals(actual.enforceDnsUid, expected.enforceDnsUid); + assertFieldCountEquals(3, ResolverOptionsParcel.class); + } + + private void assertResolverParamsEquals(@NonNull ResolverParamsParcel actual, + @NonNull ResolverParamsParcel expected) { + assertEquals(actual.netId, expected.netId); + assertEquals(actual.sampleValiditySeconds, expected.sampleValiditySeconds); + assertEquals(actual.successThreshold, expected.successThreshold); + assertEquals(actual.minSamples, expected.minSamples); + assertEquals(actual.maxSamples, expected.maxSamples); + assertEquals(actual.baseTimeoutMsec, expected.baseTimeoutMsec); + assertEquals(actual.retryCount, expected.retryCount); + assertContainsStringsExactly(actual.servers, expected.servers); + assertContainsStringsExactly(actual.domains, expected.domains); + assertEquals(actual.tlsName, expected.tlsName); + assertContainsStringsExactly(actual.tlsServers, expected.tlsServers); + assertContainsStringsExactly(actual.tlsFingerprints, expected.tlsFingerprints); + assertEquals(actual.caCertificate, expected.caCertificate); + assertEquals(actual.tlsConnectTimeoutMs, expected.tlsConnectTimeoutMs); + assertResolverOptionsEquals(actual.resolverOptions, expected.resolverOptions); + assertContainsExactly(actual.transportTypes, expected.transportTypes); + assertFieldCountEquals(16, ResolverParamsParcel.class); + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mContentResolver = new MockContentResolver(); + mContentResolver.addProvider(Settings.AUTHORITY, + new FakeSettingsProvider()); + when(mCtx.getContentResolver()).thenReturn(mContentResolver); + mDnsManager = new DnsManager(mCtx, mMockDnsResolver); + + // Clear the private DNS settings + Settings.Global.putString(mContentResolver, PRIVATE_DNS_DEFAULT_MODE, ""); + Settings.Global.putString(mContentResolver, PRIVATE_DNS_MODE, ""); + Settings.Global.putString(mContentResolver, PRIVATE_DNS_SPECIFIER, ""); + } + + @Test + public void testTrackedValidationUpdates() throws Exception { + mDnsManager.updatePrivateDns(new Network(TEST_NETID), + mDnsManager.getPrivateDnsConfig()); + mDnsManager.updatePrivateDns(new Network(TEST_NETID_ALTERNATE), + mDnsManager.getPrivateDnsConfig()); + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(TEST_IFACENAME); + lp.addDnsServer(InetAddress.getByName("3.3.3.3")); + lp.addDnsServer(InetAddress.getByName("4.4.4.4")); + + // Send a validation event that is tracked on the alternate netId + mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES); + mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp); + mDnsManager.flushVmDnsCache(); + mDnsManager.updateTransportsForNetwork(TEST_NETID_ALTERNATE, TEST_TRANSPORT_TYPES); + mDnsManager.noteDnsServersForNetwork(TEST_NETID_ALTERNATE, lp); + mDnsManager.flushVmDnsCache(); + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID_ALTERNATE, + InetAddress.parseNumericAddress("4.4.4.4"), "", + VALIDATION_RESULT_SUCCESS)); + LinkProperties fixedLp = new LinkProperties(lp); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, fixedLp); + assertFalse(fixedLp.isPrivateDnsActive()); + assertNull(fixedLp.getPrivateDnsServerName()); + fixedLp = new LinkProperties(lp); + mDnsManager.updatePrivateDnsStatus(TEST_NETID_ALTERNATE, fixedLp); + assertTrue(fixedLp.isPrivateDnsActive()); + assertNull(fixedLp.getPrivateDnsServerName()); + assertEquals(Arrays.asList(InetAddress.getByName("4.4.4.4")), + fixedLp.getValidatedPrivateDnsServers()); + + // Set up addresses for strict mode and switch to it. + lp.addLinkAddress(new LinkAddress("192.0.2.4/24")); + lp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("192.0.2.4"), + TEST_IFACENAME)); + lp.addLinkAddress(new LinkAddress("2001:db8:1::1/64")); + lp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("2001:db8:1::1"), + TEST_IFACENAME)); + + ConnectivitySettingsManager.setPrivateDnsMode(mCtx, PRIVATE_DNS_MODE_PROVIDER_HOSTNAME); + ConnectivitySettingsManager.setPrivateDnsHostname(mCtx, "strictmode.com"); + mDnsManager.updatePrivateDns(new Network(TEST_NETID), + new PrivateDnsConfig("strictmode.com", new InetAddress[] { + InetAddress.parseNumericAddress("6.6.6.6"), + InetAddress.parseNumericAddress("2001:db8:66:66::1") + })); + mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES); + mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp); + mDnsManager.flushVmDnsCache(); + fixedLp = new LinkProperties(lp); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, fixedLp); + assertTrue(fixedLp.isPrivateDnsActive()); + assertEquals("strictmode.com", fixedLp.getPrivateDnsServerName()); + // No validation events yet. + assertEquals(Arrays.asList(new InetAddress[0]), fixedLp.getValidatedPrivateDnsServers()); + // Validate one. + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID, + InetAddress.parseNumericAddress("6.6.6.6"), "strictmode.com", + VALIDATION_RESULT_SUCCESS)); + fixedLp = new LinkProperties(lp); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, fixedLp); + assertEquals(Arrays.asList(InetAddress.parseNumericAddress("6.6.6.6")), + fixedLp.getValidatedPrivateDnsServers()); + // Validate the 2nd one. + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID, + InetAddress.parseNumericAddress("2001:db8:66:66::1"), "strictmode.com", + VALIDATION_RESULT_SUCCESS)); + fixedLp = new LinkProperties(lp); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, fixedLp); + assertEquals(Arrays.asList( + InetAddress.parseNumericAddress("2001:db8:66:66::1"), + InetAddress.parseNumericAddress("6.6.6.6")), + fixedLp.getValidatedPrivateDnsServers()); + } + + @Test + public void testIgnoreUntrackedValidationUpdates() throws Exception { + // The PrivateDnsConfig map is empty, so no validation events will + // be tracked. + LinkProperties lp = new LinkProperties(); + lp.addDnsServer(InetAddress.getByName("3.3.3.3")); + mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES); + mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp); + mDnsManager.flushVmDnsCache(); + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID, + InetAddress.parseNumericAddress("3.3.3.3"), "", + VALIDATION_RESULT_SUCCESS)); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp); + assertFalse(lp.isPrivateDnsActive()); + assertNull(lp.getPrivateDnsServerName()); + + // Validation event has untracked netId + mDnsManager.updatePrivateDns(new Network(TEST_NETID), + mDnsManager.getPrivateDnsConfig()); + mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES); + mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp); + mDnsManager.flushVmDnsCache(); + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID_UNTRACKED, + InetAddress.parseNumericAddress("3.3.3.3"), "", + VALIDATION_RESULT_SUCCESS)); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp); + assertFalse(lp.isPrivateDnsActive()); + assertNull(lp.getPrivateDnsServerName()); + + // Validation event has untracked ipAddress + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID, + InetAddress.parseNumericAddress("4.4.4.4"), "", + VALIDATION_RESULT_SUCCESS)); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp); + assertFalse(lp.isPrivateDnsActive()); + assertNull(lp.getPrivateDnsServerName()); + + // Validation event has untracked hostname + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID, + InetAddress.parseNumericAddress("3.3.3.3"), "hostname", + VALIDATION_RESULT_SUCCESS)); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp); + assertFalse(lp.isPrivateDnsActive()); + assertNull(lp.getPrivateDnsServerName()); + + // Validation event failed + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID, + InetAddress.parseNumericAddress("3.3.3.3"), "", + VALIDATION_RESULT_FAILURE)); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp); + assertFalse(lp.isPrivateDnsActive()); + assertNull(lp.getPrivateDnsServerName()); + + // Network removed + mDnsManager.removeNetwork(new Network(TEST_NETID)); + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID, + InetAddress.parseNumericAddress("3.3.3.3"), "", VALIDATION_RESULT_SUCCESS)); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp); + assertFalse(lp.isPrivateDnsActive()); + assertNull(lp.getPrivateDnsServerName()); + + // Turn private DNS mode off + ConnectivitySettingsManager.setPrivateDnsMode(mCtx, PRIVATE_DNS_MODE_OFF); + mDnsManager.updatePrivateDns(new Network(TEST_NETID), + mDnsManager.getPrivateDnsConfig()); + mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES); + mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp); + mDnsManager.flushVmDnsCache(); + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID, + InetAddress.parseNumericAddress("3.3.3.3"), "", + VALIDATION_RESULT_SUCCESS)); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp); + assertFalse(lp.isPrivateDnsActive()); + assertNull(lp.getPrivateDnsServerName()); + } + + @Test + public void testOverrideDefaultMode() throws Exception { + // Hard-coded default is opportunistic mode. + final PrivateDnsConfig cfgAuto = DnsManager.getPrivateDnsConfig(mCtx); + assertTrue(cfgAuto.useTls); + assertEquals("", cfgAuto.hostname); + assertEquals(new InetAddress[0], cfgAuto.ips); + + // Pretend a gservices push sets the default to "off". + ConnectivitySettingsManager.setPrivateDnsDefaultMode(mCtx, PRIVATE_DNS_MODE_OFF); + final PrivateDnsConfig cfgOff = DnsManager.getPrivateDnsConfig(mCtx); + assertFalse(cfgOff.useTls); + assertEquals("", cfgOff.hostname); + assertEquals(new InetAddress[0], cfgOff.ips); + + // Strict mode still works. + ConnectivitySettingsManager.setPrivateDnsMode(mCtx, PRIVATE_DNS_MODE_PROVIDER_HOSTNAME); + ConnectivitySettingsManager.setPrivateDnsHostname(mCtx, "strictmode.com"); + final PrivateDnsConfig cfgStrict = DnsManager.getPrivateDnsConfig(mCtx); + assertTrue(cfgStrict.useTls); + assertEquals("strictmode.com", cfgStrict.hostname); + assertEquals(new InetAddress[0], cfgStrict.ips); + } + + @Test + public void testSendDnsConfiguration() throws Exception { + reset(mMockDnsResolver); + mDnsManager.updatePrivateDns(new Network(TEST_NETID), + mDnsManager.getPrivateDnsConfig()); + final LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(TEST_IFACENAME); + lp.addDnsServer(InetAddress.getByName("3.3.3.3")); + lp.addDnsServer(InetAddress.getByName("4.4.4.4")); + mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES); + mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp); + mDnsManager.flushVmDnsCache(); + + final ArgumentCaptor resolverParamsParcelCaptor = + ArgumentCaptor.forClass(ResolverParamsParcel.class); + verify(mMockDnsResolver, times(1)).setResolverConfiguration( + resolverParamsParcelCaptor.capture()); + final ResolverParamsParcel actualParams = resolverParamsParcelCaptor.getValue(); + final ResolverParamsParcel expectedParams = new ResolverParamsParcel(); + expectedParams.netId = TEST_NETID; + expectedParams.sampleValiditySeconds = TEST_DEFAULT_SAMPLE_VALIDITY_SECONDS; + expectedParams.successThreshold = TEST_DEFAULT_SUCCESS_THRESHOLD_PERCENT; + expectedParams.minSamples = TEST_DEFAULT_MIN_SAMPLES; + expectedParams.maxSamples = TEST_DEFAULT_MAX_SAMPLES; + expectedParams.servers = new String[]{"3.3.3.3", "4.4.4.4"}; + expectedParams.domains = new String[]{}; + expectedParams.tlsName = ""; + expectedParams.tlsServers = new String[]{"3.3.3.3", "4.4.4.4"}; + expectedParams.transportTypes = TEST_TRANSPORT_TYPES; + expectedParams.resolverOptions = new ResolverOptionsParcel(); + assertResolverParamsEquals(actualParams, expectedParams); + } + + @Test + public void testTransportTypesEqual() throws Exception { + SparseArray ncTransTypes = MessageUtils.findMessageNames( + new Class[] { NetworkCapabilities.class }, new String[]{ "TRANSPORT_" }); + SparseArray dnsTransTypes = MessageUtils.findMessageNames( + new Class[] { IDnsResolver.class }, new String[]{ "TRANSPORT_" }); + assertEquals(0, MIN_TRANSPORT); + assertEquals(MAX_TRANSPORT + 1, ncTransTypes.size()); + // TRANSPORT_UNKNOWN in IDnsResolver is defined to -1 and only for resolver. + assertEquals("TRANSPORT_UNKNOWN", dnsTransTypes.get(-1)); + assertEquals(ncTransTypes.size(), dnsTransTypes.size() - 1); + for (int i = MIN_TRANSPORT; i < MAX_TRANSPORT; i++) { + String name = ncTransTypes.get(i, null); + assertNotNull("Could not find NetworkCapabilies.TRANSPORT_* constant equal to " + + i, name); + assertEquals(name, dnsTransTypes.get(i)); + } + } + + @Test + public void testGetPrivateDnsConfigForNetwork() throws Exception { + final Network network = new Network(TEST_NETID); + final InetAddress dnsAddr = InetAddressUtils.parseNumericAddress("3.3.3.3"); + final InetAddress[] tlsAddrs = new InetAddress[]{ + InetAddressUtils.parseNumericAddress("6.6.6.6"), + InetAddressUtils.parseNumericAddress("2001:db8:66:66::1") + }; + final String tlsName = "strictmode.com"; + LinkProperties lp = new LinkProperties(); + lp.addDnsServer(dnsAddr); + + // The PrivateDnsConfig map is empty, so the default PRIVATE_DNS_OFF is returned. + PrivateDnsConfig privateDnsCfg = mDnsManager.getPrivateDnsConfig(network); + assertFalse(privateDnsCfg.useTls); + assertEquals("", privateDnsCfg.hostname); + assertEquals(new InetAddress[0], privateDnsCfg.ips); + + // An entry with default PrivateDnsConfig is added to the PrivateDnsConfig map. + mDnsManager.updatePrivateDns(network, mDnsManager.getPrivateDnsConfig()); + mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp); + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID, dnsAddr, "", + VALIDATION_RESULT_SUCCESS)); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp); + privateDnsCfg = mDnsManager.getPrivateDnsConfig(network); + assertTrue(privateDnsCfg.useTls); + assertEquals("", privateDnsCfg.hostname); + assertEquals(new InetAddress[0], privateDnsCfg.ips); + + // The original entry is overwritten by a new PrivateDnsConfig. + mDnsManager.updatePrivateDns(network, new PrivateDnsConfig(tlsName, tlsAddrs)); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp); + privateDnsCfg = mDnsManager.getPrivateDnsConfig(network); + assertTrue(privateDnsCfg.useTls); + assertEquals(tlsName, privateDnsCfg.hostname); + assertEquals(tlsAddrs, privateDnsCfg.ips); + + // The network is removed, so the PrivateDnsConfig map becomes empty again. + mDnsManager.removeNetwork(network); + privateDnsCfg = mDnsManager.getPrivateDnsConfig(network); + assertFalse(privateDnsCfg.useTls); + assertEquals("", privateDnsCfg.hostname); + assertEquals(new InetAddress[0], privateDnsCfg.ips); + } +} diff --git a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt new file mode 100644 index 0000000000..eb3b4df1a2 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2021 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 android.net.NetworkAgentConfig +import android.net.NetworkCapabilities +import android.text.TextUtils +import android.util.ArraySet +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.server.connectivity.FullScore.MAX_CS_MANAGED_POLICY +import com.android.server.connectivity.FullScore.POLICY_ACCEPT_UNVALIDATED +import com.android.server.connectivity.FullScore.POLICY_EVER_USER_SELECTED +import com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED +import com.android.server.connectivity.FullScore.POLICY_IS_VPN +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.collections.minOfOrNull +import kotlin.collections.maxOfOrNull +import kotlin.reflect.full.staticProperties +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +@SmallTest +class FullScoreTest { + // Convenience methods + fun FullScore.withPolicies( + validated: Boolean = false, + vpn: Boolean = false, + onceChosen: Boolean = false, + acceptUnvalidated: Boolean = false + ): FullScore { + val nac = NetworkAgentConfig.Builder().apply { + setUnvalidatedConnectivityAcceptable(acceptUnvalidated) + setExplicitlySelected(onceChosen) + }.build() + val nc = NetworkCapabilities.Builder().apply { + if (vpn) addTransportType(NetworkCapabilities.TRANSPORT_VPN) + if (validated) addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + }.build() + return mixInScore(nc, nac) + } + + @Test + fun testGetLegacyInt() { + val ns = FullScore(50, 0L /* policy */) + assertEquals(10, ns.legacyInt) // -40 penalty for not being validated + assertEquals(50, ns.legacyIntAsValidated) + + val vpnNs = FullScore(101, 0L /* policy */).withPolicies(vpn = true) + assertEquals(101, vpnNs.legacyInt) // VPNs are not subject to unvalidation penalty + assertEquals(101, vpnNs.legacyIntAsValidated) + assertEquals(101, vpnNs.withPolicies(validated = true).legacyInt) + assertEquals(101, vpnNs.withPolicies(validated = true).legacyIntAsValidated) + + val validatedNs = ns.withPolicies(validated = true) + assertEquals(50, validatedNs.legacyInt) // No penalty, this is validated + assertEquals(50, validatedNs.legacyIntAsValidated) + + val chosenNs = ns.withPolicies(onceChosen = true) + assertEquals(10, chosenNs.legacyInt) + assertEquals(100, chosenNs.legacyIntAsValidated) + assertEquals(10, chosenNs.withPolicies(acceptUnvalidated = true).legacyInt) + assertEquals(50, chosenNs.withPolicies(acceptUnvalidated = true).legacyIntAsValidated) + } + + @Test + fun testToString() { + val string = FullScore(10, 0L /* policy */) + .withPolicies(vpn = true, acceptUnvalidated = true).toString() + assertTrue(string.contains("Score(10"), string) + assertTrue(string.contains("ACCEPT_UNVALIDATED"), string) + assertTrue(string.contains("IS_VPN"), string) + assertFalse(string.contains("IS_VALIDATED"), string) + val foundNames = ArraySet() + getAllPolicies().forEach { + val name = FullScore.policyNameOf(it.get() as Int) + assertFalse(TextUtils.isEmpty(name)) + assertFalse(foundNames.contains(name)) + foundNames.add(name) + } + assertFailsWith { + FullScore.policyNameOf(MAX_CS_MANAGED_POLICY + 1) + } + } + + fun getAllPolicies() = Regex("POLICY_.*").let { nameRegex -> + FullScore::class.staticProperties.filter { it.name.matches(nameRegex) } + } + + @Test + fun testHasPolicy() { + val ns = FullScore(50, 0L /* policy */) + assertFalse(ns.hasPolicy(POLICY_IS_VALIDATED)) + assertFalse(ns.hasPolicy(POLICY_IS_VPN)) + assertFalse(ns.hasPolicy(POLICY_EVER_USER_SELECTED)) + assertFalse(ns.hasPolicy(POLICY_ACCEPT_UNVALIDATED)) + assertTrue(ns.withPolicies(validated = true).hasPolicy(POLICY_IS_VALIDATED)) + assertTrue(ns.withPolicies(vpn = true).hasPolicy(POLICY_IS_VPN)) + assertTrue(ns.withPolicies(onceChosen = true).hasPolicy(POLICY_EVER_USER_SELECTED)) + assertTrue(ns.withPolicies(acceptUnvalidated = true).hasPolicy(POLICY_ACCEPT_UNVALIDATED)) + } + + @Test + fun testMinMaxPolicyConstants() { + val policies = getAllPolicies() + + policies.forEach { policy -> + assertTrue(policy.get() as Int >= FullScore.MIN_CS_MANAGED_POLICY) + assertTrue(policy.get() as Int <= FullScore.MAX_CS_MANAGED_POLICY) + } + assertEquals(FullScore.MIN_CS_MANAGED_POLICY, + policies.minOfOrNull { it.get() as Int }) + assertEquals(FullScore.MAX_CS_MANAGED_POLICY, + policies.maxOfOrNull { it.get() as Int }) + } +} diff --git a/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java b/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java new file mode 100644 index 0000000000..70495cced5 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java @@ -0,0 +1,561 @@ +/* + * Copyright (C) 2016 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 com.android.server.connectivity.MetricsTestUtil.aLong; +import static com.android.server.connectivity.MetricsTestUtil.aString; +import static com.android.server.connectivity.MetricsTestUtil.aType; +import static com.android.server.connectivity.MetricsTestUtil.anInt; +import static com.android.server.connectivity.MetricsTestUtil.describeIpEvent; +import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.BLUETOOTH; +import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.CELLULAR; +import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityLog; +import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.MULTIPLE; +import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.WIFI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import android.net.ConnectivityMetricsEvent; +import android.net.metrics.ApfProgramEvent; +import android.net.metrics.ApfStats; +import android.net.metrics.DefaultNetworkEvent; +import android.net.metrics.DhcpClientEvent; +import android.net.metrics.DhcpErrorEvent; +import android.net.metrics.IpManagerEvent; +import android.net.metrics.IpReachabilityEvent; +import android.net.metrics.NetworkEvent; +import android.net.metrics.RaEvent; +import android.net.metrics.ValidationProbeEvent; +import android.net.metrics.WakeupStats; +import android.test.suitebuilder.annotation.SmallTest; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.List; + +// TODO: instead of comparing textpb to textpb, parse textpb and compare proto to proto. +@RunWith(AndroidJUnit4.class) +@SmallTest +public class IpConnectivityEventBuilderTest { + + @Test + public void testLinkLayerInferrence() { + ConnectivityMetricsEvent ev = describeIpEvent( + aType(IpReachabilityEvent.class), + anInt(IpReachabilityEvent.NUD_FAILED)); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 0", + " network_id: 0", + " time_ms: 1", + " transports: 0", + " ip_reachability_event <", + " event_type: 512", + " if_name: \"\"", + " >", + ">", + "version: 2\n"); + verifySerialization(want, ev); + + ev.netId = 123; + ev.transports = 3; // transports have priority for inferrence of link layer + ev.ifname = "wlan0"; + want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + String.format(" link_layer: %d", MULTIPLE), + " network_id: 123", + " time_ms: 1", + " transports: 3", + " ip_reachability_event <", + " event_type: 512", + " if_name: \"\"", + " >", + ">", + "version: 2\n"); + verifySerialization(want, ev); + + ev.transports = 1; + ev.ifname = null; + want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + String.format(" link_layer: %d", CELLULAR), + " network_id: 123", + " time_ms: 1", + " transports: 1", + " ip_reachability_event <", + " event_type: 512", + " if_name: \"\"", + " >", + ">", + "version: 2\n"); + verifySerialization(want, ev); + + ev.transports = 0; + ev.ifname = "not_inferred"; + want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"not_inferred\"", + " link_layer: 0", + " network_id: 123", + " time_ms: 1", + " transports: 0", + " ip_reachability_event <", + " event_type: 512", + " if_name: \"\"", + " >", + ">", + "version: 2\n"); + verifySerialization(want, ev); + + ev.ifname = "bt-pan"; + want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + String.format(" link_layer: %d", BLUETOOTH), + " network_id: 123", + " time_ms: 1", + " transports: 0", + " ip_reachability_event <", + " event_type: 512", + " if_name: \"\"", + " >", + ">", + "version: 2\n"); + verifySerialization(want, ev); + + ev.ifname = "rmnet_ipa0"; + want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + String.format(" link_layer: %d", CELLULAR), + " network_id: 123", + " time_ms: 1", + " transports: 0", + " ip_reachability_event <", + " event_type: 512", + " if_name: \"\"", + " >", + ">", + "version: 2\n"); + verifySerialization(want, ev); + + ev.ifname = "wlan0"; + want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + String.format(" link_layer: %d", WIFI), + " network_id: 123", + " time_ms: 1", + " transports: 0", + " ip_reachability_event <", + " event_type: 512", + " if_name: \"\"", + " >", + ">", + "version: 2\n"); + verifySerialization(want, ev); + } + + @Test + public void testDefaultNetworkEventSerialization() { + DefaultNetworkEvent ev = new DefaultNetworkEvent(1001); + ev.netId = 102; + ev.transports = 2; + ev.previousTransports = 4; + ev.ipv4 = true; + ev.initialScore = 20; + ev.finalScore = 60; + ev.durationMs = 54; + ev.validatedMs = 27; + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 102", + " time_ms: 0", + " transports: 2", + " default_network_event <", + " default_network_duration_ms: 54", + " final_score: 60", + " initial_score: 20", + " ip_support: 1", + " no_default_network_duration_ms: 0", + " previous_default_network_link_layer: 1", + " previous_network_ip_support: 0", + " validation_duration_ms: 27", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, IpConnectivityEventBuilder.toProto(ev)); + } + + @Test + public void testDhcpClientEventSerialization() { + ConnectivityMetricsEvent ev = describeIpEvent( + aType(DhcpClientEvent.class), + aString("SomeState"), + anInt(192)); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 0", + " network_id: 0", + " time_ms: 1", + " transports: 0", + " dhcp_event <", + " duration_ms: 192", + " if_name: \"\"", + " state_transition: \"SomeState\"", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, ev); + } + + @Test + public void testDhcpErrorEventSerialization() { + ConnectivityMetricsEvent ev = describeIpEvent( + aType(DhcpErrorEvent.class), + anInt(DhcpErrorEvent.L4_NOT_UDP)); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 0", + " network_id: 0", + " time_ms: 1", + " transports: 0", + " dhcp_event <", + " duration_ms: 0", + " if_name: \"\"", + " error_code: 50397184", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, ev); + } + + @Test + public void testIpManagerEventSerialization() { + ConnectivityMetricsEvent ev = describeIpEvent( + aType(IpManagerEvent.class), + anInt(IpManagerEvent.PROVISIONING_OK), + aLong(5678)); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 0", + " network_id: 0", + " time_ms: 1", + " transports: 0", + " ip_provisioning_event <", + " event_type: 1", + " if_name: \"\"", + " latency_ms: 5678", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, ev); + } + + @Test + public void testIpReachabilityEventSerialization() { + ConnectivityMetricsEvent ev = describeIpEvent( + aType(IpReachabilityEvent.class), + anInt(IpReachabilityEvent.NUD_FAILED)); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 0", + " network_id: 0", + " time_ms: 1", + " transports: 0", + " ip_reachability_event <", + " event_type: 512", + " if_name: \"\"", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, ev); + } + + @Test + public void testNetworkEventSerialization() { + ConnectivityMetricsEvent ev = describeIpEvent( + aType(NetworkEvent.class), + anInt(5), + aLong(20410)); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 0", + " network_id: 0", + " time_ms: 1", + " transports: 0", + " network_event <", + " event_type: 5", + " latency_ms: 20410", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, ev); + } + + @Test + public void testValidationProbeEventSerialization() { + ConnectivityMetricsEvent ev = describeIpEvent( + aType(ValidationProbeEvent.class), + aLong(40730), + anInt(ValidationProbeEvent.PROBE_HTTP), + anInt(204)); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 0", + " network_id: 0", + " time_ms: 1", + " transports: 0", + " validation_probe_event <", + " latency_ms: 40730", + " probe_result: 204", + " probe_type: 1", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, ev); + } + + @Test + public void testApfProgramEventSerialization() { + ConnectivityMetricsEvent ev = describeIpEvent( + aType(ApfProgramEvent.class), + aLong(200), + aLong(18), + anInt(7), + anInt(9), + anInt(2048), + anInt(3)); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 0", + " network_id: 0", + " time_ms: 1", + " transports: 0", + " apf_program_event <", + " current_ras: 9", + " drop_multicast: true", + " effective_lifetime: 18", + " filtered_ras: 7", + " has_ipv4_addr: true", + " lifetime: 200", + " program_length: 2048", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, ev); + } + + @Test + public void testApfStatsSerialization() { + ConnectivityMetricsEvent ev = describeIpEvent( + aType(ApfStats.class), + aLong(45000), + anInt(10), + anInt(2), + anInt(2), + anInt(1), + anInt(2), + anInt(4), + anInt(7), + anInt(3), + anInt(2048)); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 0", + " network_id: 0", + " time_ms: 1", + " transports: 0", + " apf_statistics <", + " dropped_ras: 2", + " duration_ms: 45000", + " matching_ras: 2", + " max_program_size: 2048", + " parse_errors: 2", + " program_updates: 4", + " program_updates_all: 7", + " program_updates_allowing_multicast: 3", + " received_ras: 10", + " total_packet_dropped: 0", + " total_packet_processed: 0", + " zero_lifetime_ras: 1", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, ev); + } + + @Test + public void testRaEventSerialization() { + ConnectivityMetricsEvent ev = describeIpEvent( + aType(RaEvent.class), + aLong(2000), + aLong(400), + aLong(300), + aLong(-1), + aLong(1000), + aLong(-1)); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 0", + " network_id: 0", + " time_ms: 1", + " transports: 0", + " ra_event <", + " dnssl_lifetime: -1", + " prefix_preferred_lifetime: 300", + " prefix_valid_lifetime: 400", + " rdnss_lifetime: 1000", + " route_info_lifetime: -1", + " router_lifetime: 2000", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, ev); + } + + @Test + public void testWakeupStatsSerialization() { + WakeupStats stats = new WakeupStats("wlan0"); + stats.totalWakeups = 14; + stats.applicationWakeups = 5; + stats.nonApplicationWakeups = 1; + stats.rootWakeups = 2; + stats.systemWakeups = 3; + stats.noUidWakeups = 3; + stats.l2UnicastCount = 5; + stats.l2MulticastCount = 1; + stats.l2BroadcastCount = 2; + stats.ethertypes.put(0x800, 3); + stats.ethertypes.put(0x86dd, 3); + stats.ipNextHeaders.put(6, 5); + + + IpConnectivityEvent got = IpConnectivityEventBuilder.toProto(stats); + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 0", + " time_ms: 0", + " transports: 0", + " wakeup_stats <", + " application_wakeups: 5", + " duration_sec: 0", + " ethertype_counts <", + " key: 2048", + " value: 3", + " >", + " ethertype_counts <", + " key: 34525", + " value: 3", + " >", + " ip_next_header_counts <", + " key: 6", + " value: 5", + " >", + " l2_broadcast_count: 2", + " l2_multicast_count: 1", + " l2_unicast_count: 5", + " no_uid_wakeups: 3", + " non_application_wakeups: 1", + " root_wakeups: 2", + " system_wakeups: 3", + " total_wakeups: 14", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, got); + } + + static void verifySerialization(String want, ConnectivityMetricsEvent... input) { + List protoInput = + IpConnectivityEventBuilder.toProto(Arrays.asList(input)); + verifySerialization(want, protoInput.toArray(new IpConnectivityEvent[0])); + } + + static void verifySerialization(String want, IpConnectivityEvent... input) { + try { + byte[] got = IpConnectivityEventBuilder.serialize(0, Arrays.asList(input)); + IpConnectivityLog log = IpConnectivityLog.parseFrom(got); + assertEquals(want, log.toString()); + } catch (Exception e) { + fail(e.toString()); + } + } +} diff --git a/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java b/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java new file mode 100644 index 0000000000..8b072c49de --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java @@ -0,0 +1,645 @@ +/* + * Copyright (C) 2016, 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.metrics.INetdEventListener.EVENT_GETADDRINFO; +import static android.net.metrics.INetdEventListener.EVENT_GETHOSTBYNAME; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.ConnectivityMetricsEvent; +import android.net.IIpConnectivityMetrics; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.RouteInfo; +import android.net.metrics.ApfProgramEvent; +import android.net.metrics.ApfStats; +import android.net.metrics.DhcpClientEvent; +import android.net.metrics.IpConnectivityLog; +import android.net.metrics.IpManagerEvent; +import android.net.metrics.IpReachabilityEvent; +import android.net.metrics.RaEvent; +import android.net.metrics.ValidationProbeEvent; +import android.os.Parcelable; +import android.system.OsConstants; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Base64; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.BitUtils; +import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass; + +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 java.io.PrintWriter; +import java.io.StringWriter; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class IpConnectivityMetricsTest { + static final IpReachabilityEvent FAKE_EV = + new IpReachabilityEvent(IpReachabilityEvent.NUD_FAILED); + + private static final String EXAMPLE_IPV4 = "192.0.2.1"; + private static final String EXAMPLE_IPV6 = "2001:db8:1200::2:1"; + + private static final byte[] MAC_ADDR = + {(byte)0x84, (byte)0xc9, (byte)0xb2, (byte)0x6a, (byte)0xed, (byte)0x4b}; + + @Mock Context mCtx; + @Mock IIpConnectivityMetrics mMockService; + @Mock ConnectivityManager mCm; + + IpConnectivityMetrics mService; + NetdEventListenerService mNetdListener; + private static final NetworkCapabilities CAPABILITIES_WIFI = new NetworkCapabilities.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .build(); + private static final NetworkCapabilities CAPABILITIES_CELL = new NetworkCapabilities.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .build(); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mService = new IpConnectivityMetrics(mCtx, (ctx) -> 2000); + mNetdListener = new NetdEventListenerService(mCm); + mService.mNetdListener = mNetdListener; + } + + @Test + public void testBufferFlushing() { + String output1 = getdump("flush"); + assertEquals("", output1); + + new IpConnectivityLog(mService.impl).log(1, FAKE_EV); + String output2 = getdump("flush"); + assertFalse("".equals(output2)); + + String output3 = getdump("flush"); + assertEquals("", output3); + } + + @Test + public void testRateLimiting() { + final IpConnectivityLog logger = new IpConnectivityLog(mService.impl); + final ApfProgramEvent ev = new ApfProgramEvent.Builder().build(); + final long fakeTimestamp = 1; + + int attempt = 100; // More than burst quota, but less than buffer size. + for (int i = 0; i < attempt; i++) { + logger.log(ev); + } + + String output1 = getdump("flush"); + assertFalse("".equals(output1)); + + for (int i = 0; i < attempt; i++) { + assertFalse("expected event to be dropped", logger.log(fakeTimestamp, ev)); + } + + String output2 = getdump("flush"); + assertEquals("", output2); + } + + private void logDefaultNetworkEvent(long timeMs, NetworkAgentInfo nai, + NetworkAgentInfo oldNai) { + final Network network = (nai != null) ? nai.network() : null; + final int score = (nai != null) ? nai.getCurrentScore() : 0; + final boolean validated = (nai != null) ? nai.lastValidated : false; + final LinkProperties lp = (nai != null) ? nai.linkProperties : null; + final NetworkCapabilities nc = (nai != null) ? nai.networkCapabilities : null; + + final Network prevNetwork = (oldNai != null) ? oldNai.network() : null; + final int prevScore = (oldNai != null) ? oldNai.getCurrentScore() : 0; + final LinkProperties prevLp = (oldNai != null) ? oldNai.linkProperties : null; + final NetworkCapabilities prevNc = (oldNai != null) ? oldNai.networkCapabilities : null; + + mService.mDefaultNetworkMetrics.logDefaultNetworkEvent(timeMs, network, score, validated, + lp, nc, prevNetwork, prevScore, prevLp, prevNc); + } + @Test + public void testDefaultNetworkEvents() throws Exception { + final long cell = BitUtils.packBits(new int[]{NetworkCapabilities.TRANSPORT_CELLULAR}); + final long wifi = BitUtils.packBits(new int[]{NetworkCapabilities.TRANSPORT_WIFI}); + + NetworkAgentInfo[][] defaultNetworks = { + // nothing -> cell + {null, makeNai(100, 10, false, true, cell)}, + // cell -> wifi + {makeNai(100, 50, true, true, cell), makeNai(101, 20, true, false, wifi)}, + // wifi -> nothing + {makeNai(101, 60, true, false, wifi), null}, + // nothing -> cell + {null, makeNai(102, 10, true, true, cell)}, + // cell -> wifi + {makeNai(102, 50, true, true, cell), makeNai(103, 20, true, false, wifi)}, + }; + + long timeMs = mService.mDefaultNetworkMetrics.creationTimeMs; + long durationMs = 1001; + for (NetworkAgentInfo[] pair : defaultNetworks) { + timeMs += durationMs; + durationMs += durationMs; + logDefaultNetworkEvent(timeMs, pair[1], pair[0]); + } + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 5", + " network_id: 0", + " time_ms: 0", + " transports: 0", + " default_network_event <", + " default_network_duration_ms: 1001", + " final_score: 0", + " initial_score: 0", + " ip_support: 0", + " no_default_network_duration_ms: 0", + " previous_default_network_link_layer: 0", + " previous_network_ip_support: 0", + " validation_duration_ms: 0", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 2", + " network_id: 100", + " time_ms: 0", + " transports: 1", + " default_network_event <", + " default_network_duration_ms: 2002", + " final_score: 50", + " initial_score: 10", + " ip_support: 3", + " no_default_network_duration_ms: 0", + " previous_default_network_link_layer: 0", + " previous_network_ip_support: 0", + " validation_duration_ms: 2002", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 101", + " time_ms: 0", + " transports: 2", + " default_network_event <", + " default_network_duration_ms: 4004", + " final_score: 60", + " initial_score: 20", + " ip_support: 1", + " no_default_network_duration_ms: 0", + " previous_default_network_link_layer: 2", + " previous_network_ip_support: 0", + " validation_duration_ms: 4004", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 5", + " network_id: 0", + " time_ms: 0", + " transports: 0", + " default_network_event <", + " default_network_duration_ms: 8008", + " final_score: 0", + " initial_score: 0", + " ip_support: 0", + " no_default_network_duration_ms: 0", + " previous_default_network_link_layer: 4", + " previous_network_ip_support: 0", + " validation_duration_ms: 0", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 2", + " network_id: 102", + " time_ms: 0", + " transports: 1", + " default_network_event <", + " default_network_duration_ms: 16016", + " final_score: 50", + " initial_score: 10", + " ip_support: 3", + " no_default_network_duration_ms: 0", + " previous_default_network_link_layer: 4", + " previous_network_ip_support: 0", + " validation_duration_ms: 16016", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, getdump("flush")); + } + + @Test + public void testEndToEndLogging() throws Exception { + // TODO: instead of comparing textpb to textpb, parse textpb and compare proto to proto. + IpConnectivityLog logger = new IpConnectivityLog(mService.impl); + + ApfStats apfStats = new ApfStats.Builder() + .setDurationMs(45000) + .setReceivedRas(10) + .setMatchingRas(2) + .setDroppedRas(2) + .setParseErrors(2) + .setZeroLifetimeRas(1) + .setProgramUpdates(4) + .setProgramUpdatesAll(7) + .setProgramUpdatesAllowingMulticast(3) + .setMaxProgramSize(2048) + .build(); + + final ValidationProbeEvent validationEv = new ValidationProbeEvent.Builder() + .setDurationMs(40730) + .setProbeType(ValidationProbeEvent.PROBE_HTTP, true) + .setReturnCode(204) + .build(); + + final DhcpClientEvent event = new DhcpClientEvent.Builder() + .setMsg("SomeState") + .setDurationMs(192) + .build(); + Parcelable[] events = { + new IpReachabilityEvent(IpReachabilityEvent.NUD_FAILED), event, + new IpManagerEvent(IpManagerEvent.PROVISIONING_OK, 5678), + validationEv, + apfStats, + new RaEvent(2000, 400, 300, -1, 1000, -1) + }; + + for (int i = 0; i < events.length; i++) { + ConnectivityMetricsEvent ev = new ConnectivityMetricsEvent(); + ev.timestamp = 100 * (i + 1); + ev.ifname = "wlan0"; + ev.data = events[i]; + logger.log(ev); + } + + // netId, errno, latency, destination + connectEvent(100, OsConstants.EALREADY, 0, EXAMPLE_IPV4); + connectEvent(100, OsConstants.EINPROGRESS, 0, EXAMPLE_IPV6); + connectEvent(100, 0, 110, EXAMPLE_IPV4); + connectEvent(101, 0, 23, EXAMPLE_IPV4); + connectEvent(101, 0, 45, EXAMPLE_IPV6); + connectEvent(100, OsConstants.EAGAIN, 0, EXAMPLE_IPV4); + + // netId, type, return code, latency + dnsEvent(100, EVENT_GETADDRINFO, 0, 3456); + dnsEvent(100, EVENT_GETADDRINFO, 3, 45); + dnsEvent(100, EVENT_GETHOSTBYNAME, 0, 638); + dnsEvent(101, EVENT_GETADDRINFO, 0, 56); + dnsEvent(101, EVENT_GETHOSTBYNAME, 0, 34); + + // iface, uid + final byte[] mac = {0x48, 0x7c, 0x2b, 0x6a, 0x3e, 0x4b}; + final String srcIp = "192.168.2.1"; + final String dstIp = "192.168.2.23"; + final int sport = 2356; + final int dport = 13489; + final long now = 1001L; + final int v4 = 0x800; + final int tcp = 6; + final int udp = 17; + wakeupEvent("wlan0", 1000, v4, tcp, mac, srcIp, dstIp, sport, dport, 1001L); + wakeupEvent("wlan0", 10123, v4, tcp, mac, srcIp, dstIp, sport, dport, 1001L); + wakeupEvent("wlan0", 1000, v4, udp, mac, srcIp, dstIp, sport, dport, 1001L); + wakeupEvent("wlan0", 10008, v4, udp, mac, srcIp, dstIp, sport, dport, 1001L); + wakeupEvent("wlan0", -1, v4, udp, mac, srcIp, dstIp, sport, dport, 1001L); + wakeupEvent("wlan0", 10008, v4, tcp, mac, srcIp, dstIp, sport, dport, 1001L); + + long timeMs = mService.mDefaultNetworkMetrics.creationTimeMs; + final long cell = BitUtils.packBits(new int[]{NetworkCapabilities.TRANSPORT_CELLULAR}); + final long wifi = BitUtils.packBits(new int[]{NetworkCapabilities.TRANSPORT_WIFI}); + NetworkAgentInfo cellNai = makeNai(100, 50, false, true, cell); + NetworkAgentInfo wifiNai = makeNai(101, 60, true, false, wifi); + logDefaultNetworkEvent(timeMs + 200L, cellNai, null); + logDefaultNetworkEvent(timeMs + 300L, wifiNai, cellNai); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 0", + " time_ms: 100", + " transports: 0", + " ip_reachability_event <", + " event_type: 512", + " if_name: \"\"", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 0", + " time_ms: 200", + " transports: 0", + " dhcp_event <", + " duration_ms: 192", + " if_name: \"\"", + " state_transition: \"SomeState\"", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 0", + " time_ms: 300", + " transports: 0", + " ip_provisioning_event <", + " event_type: 1", + " if_name: \"\"", + " latency_ms: 5678", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 0", + " time_ms: 400", + " transports: 0", + " validation_probe_event <", + " latency_ms: 40730", + " probe_result: 204", + " probe_type: 257", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 0", + " time_ms: 500", + " transports: 0", + " apf_statistics <", + " dropped_ras: 2", + " duration_ms: 45000", + " matching_ras: 2", + " max_program_size: 2048", + " parse_errors: 2", + " program_updates: 4", + " program_updates_all: 7", + " program_updates_allowing_multicast: 3", + " received_ras: 10", + " total_packet_dropped: 0", + " total_packet_processed: 0", + " zero_lifetime_ras: 1", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 0", + " time_ms: 600", + " transports: 0", + " ra_event <", + " dnssl_lifetime: -1", + " prefix_preferred_lifetime: 300", + " prefix_valid_lifetime: 400", + " rdnss_lifetime: 1000", + " route_info_lifetime: -1", + " router_lifetime: 2000", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 5", + " network_id: 0", + " time_ms: 0", + " transports: 0", + " default_network_event <", + " default_network_duration_ms: 200", + " final_score: 0", + " initial_score: 0", + " ip_support: 0", + " no_default_network_duration_ms: 0", + " previous_default_network_link_layer: 0", + " previous_network_ip_support: 0", + " validation_duration_ms: 0", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 2", + " network_id: 100", + " time_ms: 0", + " transports: 1", + " default_network_event <", + " default_network_duration_ms: 100", + " final_score: 50", + " initial_score: 50", + " ip_support: 2", + " no_default_network_duration_ms: 0", + " previous_default_network_link_layer: 0", + " previous_network_ip_support: 0", + " validation_duration_ms: 100", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 100", + " time_ms: 0", + " transports: 2", + " connect_statistics <", + " connect_blocking_count: 1", + " connect_count: 3", + " errnos_counters <", + " key: 11", + " value: 1", + " >", + " ipv6_addr_count: 1", + " latencies_ms: 110", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 2", + " network_id: 101", + " time_ms: 0", + " transports: 1", + " connect_statistics <", + " connect_blocking_count: 2", + " connect_count: 2", + " ipv6_addr_count: 1", + " latencies_ms: 23", + " latencies_ms: 45", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 100", + " time_ms: 0", + " transports: 2", + " dns_lookup_batch <", + " event_types: 1", + " event_types: 1", + " event_types: 2", + " getaddrinfo_error_count: 0", + " getaddrinfo_query_count: 0", + " gethostbyname_error_count: 0", + " gethostbyname_query_count: 0", + " latencies_ms: 3456", + " latencies_ms: 45", + " latencies_ms: 638", + " return_codes: 0", + " return_codes: 3", + " return_codes: 0", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 2", + " network_id: 101", + " time_ms: 0", + " transports: 1", + " dns_lookup_batch <", + " event_types: 1", + " event_types: 2", + " getaddrinfo_error_count: 0", + " getaddrinfo_query_count: 0", + " gethostbyname_error_count: 0", + " gethostbyname_query_count: 0", + " latencies_ms: 56", + " latencies_ms: 34", + " return_codes: 0", + " return_codes: 0", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 0", + " time_ms: 0", + " transports: 0", + " wakeup_stats <", + " application_wakeups: 3", + " duration_sec: 0", + " ethertype_counts <", + " key: 2048", + " value: 6", + " >", + " ip_next_header_counts <", + " key: 6", + " value: 3", + " >", + " ip_next_header_counts <", + " key: 17", + " value: 3", + " >", + " l2_broadcast_count: 0", + " l2_multicast_count: 0", + " l2_unicast_count: 6", + " no_uid_wakeups: 1", + " non_application_wakeups: 0", + " root_wakeups: 0", + " system_wakeups: 2", + " total_wakeups: 6", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, getdump("flush")); + } + + String getdump(String ... command) { + StringWriter buffer = new StringWriter(); + PrintWriter writer = new PrintWriter(buffer); + mService.impl.dump(null, writer, command); + return buffer.toString(); + } + + private void setCapabilities(int netId) { + final ArgumentCaptor networkCallback = + ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class); + verify(mCm).registerNetworkCallback(any(), networkCallback.capture()); + networkCallback.getValue().onCapabilitiesChanged(new Network(netId), + netId == 100 ? CAPABILITIES_WIFI : CAPABILITIES_CELL); + } + + void connectEvent(int netId, int error, int latencyMs, String ipAddr) throws Exception { + setCapabilities(netId); + mNetdListener.onConnectEvent(netId, error, latencyMs, ipAddr, 80, 1); + } + + void dnsEvent(int netId, int type, int result, int latency) throws Exception { + setCapabilities(netId); + mNetdListener.onDnsEvent(netId, type, result, latency, "", null, 0, 0); + } + + void wakeupEvent(String iface, int uid, int ether, int ip, byte[] mac, String srcIp, + String dstIp, int sport, int dport, long now) throws Exception { + String prefix = NetdEventListenerService.WAKEUP_EVENT_IFACE_PREFIX + iface; + mNetdListener.onWakeupEvent(prefix, uid, ether, ip, mac, srcIp, dstIp, sport, dport, now); + } + + NetworkAgentInfo makeNai(int netId, int score, boolean ipv4, boolean ipv6, long transports) { + NetworkAgentInfo nai = mock(NetworkAgentInfo.class); + when(nai.network()).thenReturn(new Network(netId)); + when(nai.getCurrentScore()).thenReturn(score); + nai.linkProperties = new LinkProperties(); + nai.networkCapabilities = new NetworkCapabilities(); + nai.lastValidated = true; + for (int t : BitUtils.unpackBits(transports)) { + nai.networkCapabilities.addTransportType(t); + } + if (ipv4) { + nai.linkProperties.addLinkAddress(new LinkAddress("192.0.2.12/24")); + nai.linkProperties.addRoute(new RouteInfo(new IpPrefix("0.0.0.0/0"))); + } + if (ipv6) { + nai.linkProperties.addLinkAddress(new LinkAddress("2001:db8:dead:beef:f00::a0/64")); + nai.linkProperties.addRoute(new RouteInfo(new IpPrefix("::/0"))); + } + return nai; + } + + + + static void verifySerialization(String want, String output) { + try { + byte[] got = Base64.decode(output, Base64.DEFAULT); + IpConnectivityLogClass.IpConnectivityLog log = + IpConnectivityLogClass.IpConnectivityLog.parseFrom(got); + assertEquals(want, log.toString()); + } catch (Exception e) { + fail(e.toString()); + } + } +} diff --git a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java new file mode 100644 index 0000000000..116d755e30 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java @@ -0,0 +1,393 @@ +/* + * Copyright (C) 2016, 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 org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.net.ConnectivityResources; +import android.net.IDnsResolver; +import android.net.INetd; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkAgentConfig; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.NetworkProvider; +import android.net.NetworkScore; +import android.os.Binder; +import android.text.format.DateUtils; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.connectivity.resources.R; +import com.android.server.ConnectivityService; +import com.android.server.connectivity.NetworkNotificationManager.NotificationType; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class LingerMonitorTest { + static final String CELLULAR = "CELLULAR"; + static final String WIFI = "WIFI"; + + static final long LOW_RATE_LIMIT = DateUtils.MINUTE_IN_MILLIS; + static final long HIGH_RATE_LIMIT = 0; + + static final int LOW_DAILY_LIMIT = 2; + static final int HIGH_DAILY_LIMIT = 1000; + + LingerMonitor mMonitor; + + @Mock ConnectivityService mConnService; + @Mock IDnsResolver mDnsResolver; + @Mock INetd mNetd; + @Mock Context mCtx; + @Mock NetworkNotificationManager mNotifier; + @Mock Resources mResources; + @Mock QosCallbackTracker mQosCallbackTracker; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mCtx.getResources()).thenReturn(mResources); + when(mCtx.getPackageName()).thenReturn("com.android.server.connectivity"); + ConnectivityResources.setResourcesContextForTest(mCtx); + + mMonitor = new TestableLingerMonitor(mCtx, mNotifier, HIGH_DAILY_LIMIT, HIGH_RATE_LIMIT); + } + + @After + public void tearDown() { + ConnectivityResources.setResourcesContextForTest(null); + } + + @Test + public void testTransitions() { + setNotificationSwitch(transition(WIFI, CELLULAR)); + NetworkAgentInfo nai1 = wifiNai(100); + NetworkAgentInfo nai2 = cellNai(101); + + assertTrue(mMonitor.isNotificationEnabled(nai1, nai2)); + assertFalse(mMonitor.isNotificationEnabled(nai2, nai1)); + } + + @Test + public void testNotificationOnLinger() { + setNotificationSwitch(transition(WIFI, CELLULAR)); + setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNotification(from, to); + } + + @Test + public void testToastOnLinger() { + setNotificationSwitch(transition(WIFI, CELLULAR)); + setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyToast(from, to); + } + + @Test + public void testNotificationClearedAfterDisconnect() { + setNotificationSwitch(transition(WIFI, CELLULAR)); + setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNotification(from, to); + + mMonitor.noteDisconnect(to); + verify(mNotifier, times(1)).clearNotification(100); + } + + @Test + public void testNotificationClearedAfterSwitchingBack() { + setNotificationSwitch(transition(WIFI, CELLULAR)); + setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNotification(from, to); + + mMonitor.noteLingerDefaultNetwork(to, from); + verify(mNotifier, times(1)).clearNotification(100); + } + + @Test + public void testUniqueToast() { + setNotificationSwitch(transition(WIFI, CELLULAR)); + setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyToast(from, to); + + mMonitor.noteLingerDefaultNetwork(to, from); + verify(mNotifier, times(1)).clearNotification(100); + + reset(mNotifier); + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNoNotifications(); + } + + @Test + public void testMultipleNotifications() { + setNotificationSwitch(transition(WIFI, CELLULAR)); + setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION); + NetworkAgentInfo wifi1 = wifiNai(100); + NetworkAgentInfo wifi2 = wifiNai(101); + NetworkAgentInfo cell = cellNai(102); + + mMonitor.noteLingerDefaultNetwork(wifi1, cell); + verifyNotification(wifi1, cell); + + mMonitor.noteLingerDefaultNetwork(cell, wifi2); + verify(mNotifier, times(1)).clearNotification(100); + + reset(mNotifier); + mMonitor.noteLingerDefaultNetwork(wifi2, cell); + verifyNotification(wifi2, cell); + } + + @Test + public void testRateLimiting() throws InterruptedException { + mMonitor = new TestableLingerMonitor(mCtx, mNotifier, HIGH_DAILY_LIMIT, LOW_RATE_LIMIT); + + setNotificationSwitch(transition(WIFI, CELLULAR)); + setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION); + NetworkAgentInfo wifi1 = wifiNai(100); + NetworkAgentInfo wifi2 = wifiNai(101); + NetworkAgentInfo wifi3 = wifiNai(102); + NetworkAgentInfo cell = cellNai(103); + + mMonitor.noteLingerDefaultNetwork(wifi1, cell); + verifyNotification(wifi1, cell); + reset(mNotifier); + + Thread.sleep(50); + mMonitor.noteLingerDefaultNetwork(cell, wifi2); + mMonitor.noteLingerDefaultNetwork(wifi2, cell); + verifyNoNotifications(); + + Thread.sleep(50); + mMonitor.noteLingerDefaultNetwork(cell, wifi3); + mMonitor.noteLingerDefaultNetwork(wifi3, cell); + verifyNoNotifications(); + } + + @Test + public void testDailyLimiting() throws InterruptedException { + mMonitor = new TestableLingerMonitor(mCtx, mNotifier, LOW_DAILY_LIMIT, HIGH_RATE_LIMIT); + + setNotificationSwitch(transition(WIFI, CELLULAR)); + setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION); + NetworkAgentInfo wifi1 = wifiNai(100); + NetworkAgentInfo wifi2 = wifiNai(101); + NetworkAgentInfo wifi3 = wifiNai(102); + NetworkAgentInfo cell = cellNai(103); + + mMonitor.noteLingerDefaultNetwork(wifi1, cell); + verifyNotification(wifi1, cell); + reset(mNotifier); + + Thread.sleep(50); + mMonitor.noteLingerDefaultNetwork(cell, wifi2); + mMonitor.noteLingerDefaultNetwork(wifi2, cell); + verifyNotification(wifi2, cell); + reset(mNotifier); + + Thread.sleep(50); + mMonitor.noteLingerDefaultNetwork(cell, wifi3); + mMonitor.noteLingerDefaultNetwork(wifi3, cell); + verifyNoNotifications(); + } + + @Test + public void testUniqueNotification() { + setNotificationSwitch(transition(WIFI, CELLULAR)); + setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNotification(from, to); + + mMonitor.noteLingerDefaultNetwork(to, from); + verify(mNotifier, times(1)).clearNotification(100); + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNotification(from, to); + } + + @Test + public void testIgnoreNeverValidatedNetworks() { + setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST); + setNotificationSwitch(transition(WIFI, CELLULAR)); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + from.everValidated = false; + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNoNotifications(); + } + + @Test + public void testIgnoreCurrentlyValidatedNetworks() { + setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST); + setNotificationSwitch(transition(WIFI, CELLULAR)); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + from.lastValidated = true; + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNoNotifications(); + } + + @Test + public void testNoNotificationType() { + setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST); + setNotificationSwitch(); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNoNotifications(); + } + + @Test + public void testNoTransitionToNotify() { + setNotificationType(LingerMonitor.NOTIFY_TYPE_NONE); + setNotificationSwitch(transition(WIFI, CELLULAR)); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNoNotifications(); + } + + @Test + public void testDifferentTransitionToNotify() { + setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST); + setNotificationSwitch(transition(CELLULAR, WIFI)); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNoNotifications(); + } + + void setNotificationSwitch(String... transitions) { + when(mResources.getStringArray(R.array.config_networkNotifySwitches)) + .thenReturn(transitions); + } + + String transition(String from, String to) { + return from + "-" + to; + } + + void setNotificationType(int type) { + when(mResources.getInteger(R.integer.config_networkNotifySwitchType)).thenReturn(type); + } + + void verifyNoToast() { + verify(mNotifier, never()).showToast(any(), any()); + } + + void verifyNoNotification() { + verify(mNotifier, never()) + .showNotification(anyInt(), any(), any(), any(), any(), anyBoolean()); + } + + void verifyNoNotifications() { + verifyNoToast(); + verifyNoNotification(); + } + + void verifyToast(NetworkAgentInfo from, NetworkAgentInfo to) { + verifyNoNotification(); + verify(mNotifier, times(1)).showToast(from, to); + } + + void verifyNotification(NetworkAgentInfo from, NetworkAgentInfo to) { + verifyNoToast(); + verify(mNotifier, times(1)).showNotification(eq(from.network.netId), + eq(NotificationType.NETWORK_SWITCH), eq(from), eq(to), any(), eq(true)); + } + + NetworkAgentInfo nai(int netId, int transport, int networkType, String networkTypeName) { + NetworkInfo info = new NetworkInfo(networkType, 0, networkTypeName, ""); + NetworkCapabilities caps = new NetworkCapabilities(); + caps.addCapability(0); + caps.addTransportType(transport); + NetworkAgentInfo nai = new NetworkAgentInfo(null, new Network(netId), info, + new LinkProperties(), caps, new NetworkScore.Builder().setLegacyInt(50).build(), + mCtx, null, new NetworkAgentConfig.Builder().build(), mConnService, mNetd, + mDnsResolver, NetworkProvider.ID_NONE, Binder.getCallingUid(), + mQosCallbackTracker, new ConnectivityService.Dependencies()); + nai.everValidated = true; + return nai; + } + + NetworkAgentInfo wifiNai(int netId) { + return nai(netId, NetworkCapabilities.TRANSPORT_WIFI, + ConnectivityManager.TYPE_WIFI, WIFI); + } + + NetworkAgentInfo cellNai(int netId) { + return nai(netId, NetworkCapabilities.TRANSPORT_CELLULAR, + ConnectivityManager.TYPE_MOBILE, CELLULAR); + } + + public static class TestableLingerMonitor extends LingerMonitor { + public TestableLingerMonitor(Context c, NetworkNotificationManager n, int l, long r) { + super(c, n, l, r); + } + @Override protected PendingIntent createNotificationIntent() { + return null; + } + } +} diff --git a/tests/unit/java/com/android/server/connectivity/MetricsTestUtil.java b/tests/unit/java/com/android/server/connectivity/MetricsTestUtil.java new file mode 100644 index 0000000000..5064b9bd91 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/MetricsTestUtil.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2016 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 android.net.ConnectivityMetricsEvent; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.function.Consumer; + +abstract public class MetricsTestUtil { + private MetricsTestUtil() { + } + + static ConnectivityMetricsEvent ev(Parcelable p) { + ConnectivityMetricsEvent ev = new ConnectivityMetricsEvent(); + ev.timestamp = 1L; + ev.data = p; + return ev; + } + + static ConnectivityMetricsEvent describeIpEvent(Consumer... fs) { + Parcel p = Parcel.obtain(); + for (Consumer f : fs) { + f.accept(p); + } + p.setDataPosition(0); + return ev(p.readParcelable(ClassLoader.getSystemClassLoader())); + } + + static Consumer aType(Class c) { + return aString(c.getName()); + } + + static Consumer aBool(boolean b) { + return aByte((byte) (b ? 1 : 0)); + } + + static Consumer aByte(byte b) { + return (p) -> p.writeByte(b); + } + + static Consumer anInt(int i) { + return (p) -> p.writeInt(i); + } + + static Consumer aLong(long l) { + return (p) -> p.writeLong(l); + } + + static Consumer aString(String s) { + return (p) -> p.writeString(s); + } + + static Consumer aByteArray(byte... ary) { + return (p) -> p.writeByteArray(ary); + } + + static Consumer anIntArray(int... ary) { + return (p) -> p.writeIntArray(ary); + } + + static byte b(int i) { + return (byte) i; + } +} diff --git a/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java new file mode 100644 index 0000000000..38f6d7f317 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java @@ -0,0 +1,381 @@ +/* + * 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 com.android.server.connectivity; + +import static android.content.Intent.ACTION_CONFIGURATION_CHANGED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkPolicy.LIMIT_DISABLED; +import static android.net.NetworkPolicy.SNOOZE_NEVER; +import static android.net.NetworkPolicy.WARNING_DISABLED; +import static android.provider.Settings.Global.NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES; + +import static com.android.server.net.NetworkPolicyManagerInternal.QUOTA_TYPE_MULTIPATH; +import static com.android.server.net.NetworkPolicyManagerService.OPPORTUNISTIC_QUOTA_UNKNOWN; + +import static junit.framework.TestCase.assertNotNull; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.usage.NetworkStatsManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.net.EthernetNetworkSpecifier; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkPolicy; +import android.net.NetworkPolicyManager; +import android.net.NetworkTemplate; +import android.net.TelephonyNetworkSpecifier; +import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; +import android.telephony.TelephonyManager; +import android.test.mock.MockContentResolver; +import android.util.DataUnit; +import android.util.RecurrenceRule; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.R; +import com.android.internal.util.test.FakeSettingsProvider; +import com.android.server.LocalServices; +import com.android.server.net.NetworkPolicyManagerInternal; +import com.android.server.net.NetworkStatsManagerInternal; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.time.Clock; +import java.time.Instant; +import java.time.Period; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class MultipathPolicyTrackerTest { + private static final Network TEST_NETWORK = new Network(123); + private static final int POLICY_SNOOZED = -100; + + @Mock private Context mContext; + @Mock private Context mUserAllContext; + @Mock private Resources mResources; + @Mock private Handler mHandler; + @Mock private MultipathPolicyTracker.Dependencies mDeps; + @Mock private Clock mClock; + @Mock private ConnectivityManager mCM; + @Mock private NetworkPolicyManager mNPM; + @Mock private NetworkStatsManager mStatsManager; + @Mock private NetworkPolicyManagerInternal mNPMI; + @Mock private NetworkStatsManagerInternal mNetworkStatsManagerInternal; + @Mock private TelephonyManager mTelephonyManager; + private MockContentResolver mContentResolver; + + private ArgumentCaptor mConfigChangeReceiverCaptor; + + private MultipathPolicyTracker mTracker; + + private Clock mPreviousRecurrenceRuleClock; + private boolean mRecurrenceRuleClockMocked; + + private void mockService(String serviceName, Class serviceClass, T service) { + when(mContext.getSystemServiceName(serviceClass)).thenReturn(serviceName); + when(mContext.getSystemService(serviceName)).thenReturn(service); + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mPreviousRecurrenceRuleClock = RecurrenceRule.sClock; + RecurrenceRule.sClock = mClock; + mRecurrenceRuleClockMocked = true; + + mConfigChangeReceiverCaptor = ArgumentCaptor.forClass(BroadcastReceiver.class); + + when(mContext.getResources()).thenReturn(mResources); + when(mContext.getApplicationInfo()).thenReturn(new ApplicationInfo()); + // Mock user id to all users that Context#registerReceiver will register with all users too. + doReturn(UserHandle.ALL.getIdentifier()).when(mUserAllContext).getUserId(); + when(mContext.createContextAsUser(eq(UserHandle.ALL), anyInt())) + .thenReturn(mUserAllContext); + when(mUserAllContext.registerReceiver(mConfigChangeReceiverCaptor.capture(), + argThat(f -> f.hasAction(ACTION_CONFIGURATION_CHANGED)), any(), any())) + .thenReturn(null); + + when(mDeps.getClock()).thenReturn(mClock); + + when(mTelephonyManager.createForSubscriptionId(anyInt())).thenReturn(mTelephonyManager); + + mContentResolver = Mockito.spy(new MockContentResolver(mContext)); + mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); + Settings.Global.clearProviderForTest(); + when(mContext.getContentResolver()).thenReturn(mContentResolver); + + mockService(Context.CONNECTIVITY_SERVICE, ConnectivityManager.class, mCM); + mockService(Context.NETWORK_POLICY_SERVICE, NetworkPolicyManager.class, mNPM); + mockService(Context.NETWORK_STATS_SERVICE, NetworkStatsManager.class, mStatsManager); + mockService(Context.TELEPHONY_SERVICE, TelephonyManager.class, mTelephonyManager); + + LocalServices.removeServiceForTest(NetworkPolicyManagerInternal.class); + LocalServices.addService(NetworkPolicyManagerInternal.class, mNPMI); + + LocalServices.removeServiceForTest(NetworkStatsManagerInternal.class); + LocalServices.addService(NetworkStatsManagerInternal.class, mNetworkStatsManagerInternal); + + mTracker = new MultipathPolicyTracker(mContext, mHandler, mDeps); + } + + @After + public void tearDown() { + // Avoid setting static clock to null (which should normally not be the case) + // if MockitoAnnotations.initMocks threw an exception + if (mRecurrenceRuleClockMocked) { + RecurrenceRule.sClock = mPreviousRecurrenceRuleClock; + } + mRecurrenceRuleClockMocked = false; + } + + private void setDefaultQuotaGlobalSetting(long setting) { + Settings.Global.putInt(mContentResolver, NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES, + (int) setting); + } + + private void testGetMultipathPreference( + long usedBytesToday, long subscriptionQuota, long policyWarning, long policyLimit, + long defaultGlobalSetting, long defaultResSetting, boolean roaming) { + + // TODO: tests should not use ZoneId.systemDefault() once code handles TZ correctly. + final ZonedDateTime now = ZonedDateTime.ofInstant( + Instant.parse("2017-04-02T10:11:12Z"), ZoneId.systemDefault()); + final ZonedDateTime startOfDay = now.truncatedTo(ChronoUnit.DAYS); + when(mClock.millis()).thenReturn(now.toInstant().toEpochMilli()); + when(mClock.instant()).thenReturn(now.toInstant()); + when(mClock.getZone()).thenReturn(ZoneId.systemDefault()); + + // Setup plan quota + when(mNPMI.getSubscriptionOpportunisticQuota(TEST_NETWORK, QUOTA_TYPE_MULTIPATH)) + .thenReturn(subscriptionQuota); + + // Setup user policy warning / limit + if (policyWarning != WARNING_DISABLED || policyLimit != LIMIT_DISABLED) { + final Instant recurrenceStart = Instant.parse("2017-04-01T00:00:00Z"); + final RecurrenceRule recurrenceRule = new RecurrenceRule( + ZonedDateTime.ofInstant( + recurrenceStart, + ZoneId.systemDefault()), + null /* end */, + Period.ofMonths(1)); + final boolean snoozeWarning = policyWarning == POLICY_SNOOZED; + final boolean snoozeLimit = policyLimit == POLICY_SNOOZED; + when(mNPM.getNetworkPolicies()).thenReturn(new NetworkPolicy[] { + new NetworkPolicy( + NetworkTemplate.buildTemplateMobileWildcard(), + recurrenceRule, + snoozeWarning ? 0 : policyWarning, + snoozeLimit ? 0 : policyLimit, + snoozeWarning ? recurrenceStart.toEpochMilli() + 1 : SNOOZE_NEVER, + snoozeLimit ? recurrenceStart.toEpochMilli() + 1 : SNOOZE_NEVER, + SNOOZE_NEVER, + true /* metered */, + false /* inferred */) + }); + } else { + when(mNPM.getNetworkPolicies()).thenReturn(new NetworkPolicy[0]); + } + + // Setup default quota in settings and resources + if (defaultGlobalSetting > 0) { + setDefaultQuotaGlobalSetting(defaultGlobalSetting); + } + when(mResources.getInteger(R.integer.config_networkDefaultDailyMultipathQuotaBytes)) + .thenReturn((int) defaultResSetting); + + when(mNetworkStatsManagerInternal.getNetworkTotalBytes( + any(), + eq(startOfDay.toInstant().toEpochMilli()), + eq(now.toInstant().toEpochMilli()))).thenReturn(usedBytesToday); + + ArgumentCaptor networkCallback = + ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class); + mTracker.start(); + verify(mCM).registerNetworkCallback(any(), networkCallback.capture(), any()); + + // Simulate callback after capability changes + NetworkCapabilities capabilities = new NetworkCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .addTransportType(TRANSPORT_CELLULAR) + .setNetworkSpecifier(new EthernetNetworkSpecifier("eth234")); + if (!roaming) { + capabilities.addCapability(NET_CAPABILITY_NOT_ROAMING); + } + networkCallback.getValue().onCapabilitiesChanged( + TEST_NETWORK, + capabilities); + + // make sure it also works with the new introduced TelephonyNetworkSpecifier + capabilities = new NetworkCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .addTransportType(TRANSPORT_CELLULAR) + .setNetworkSpecifier(new TelephonyNetworkSpecifier.Builder() + .setSubscriptionId(234).build()); + if (!roaming) { + capabilities.addCapability(NET_CAPABILITY_NOT_ROAMING); + } + networkCallback.getValue().onCapabilitiesChanged( + TEST_NETWORK, + capabilities); + } + + @Test + public void testGetMultipathPreference_SubscriptionQuota() { + testGetMultipathPreference( + DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */, + DataUnit.MEGABYTES.toBytes(14) /* subscriptionQuota */, + DataUnit.MEGABYTES.toBytes(100) /* policyWarning */, + LIMIT_DISABLED, + DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */, + 2_500_000 /* defaultResSetting */, + false /* roaming */); + + verify(mStatsManager, times(1)).registerUsageCallback( + any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any()); + } + + @Test + public void testGetMultipathPreference_UserWarningQuota() { + testGetMultipathPreference( + DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */, + OPPORTUNISTIC_QUOTA_UNKNOWN, + // 29 days from Apr. 2nd to May 1st + DataUnit.MEGABYTES.toBytes(15 * 29 * 20) /* policyWarning */, + LIMIT_DISABLED, + DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */, + 2_500_000 /* defaultResSetting */, + false /* roaming */); + + // Daily budget should be 15MB (5% of daily quota), 7MB used today: callback set for 8MB + verify(mStatsManager, times(1)).registerUsageCallback( + any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any()); + } + + @Test + public void testGetMultipathPreference_SnoozedWarningQuota() { + testGetMultipathPreference( + DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */, + OPPORTUNISTIC_QUOTA_UNKNOWN, + // 29 days from Apr. 2nd to May 1st + POLICY_SNOOZED /* policyWarning */, + DataUnit.MEGABYTES.toBytes(15 * 29 * 20) /* policyLimit */, + DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */, + 2_500_000 /* defaultResSetting */, + false /* roaming */); + + // Daily budget should be 15MB (5% of daily quota), 7MB used today: callback set for 8MB + verify(mStatsManager, times(1)).registerUsageCallback( + any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any()); + } + + @Test + public void testGetMultipathPreference_SnoozedBothQuota() { + testGetMultipathPreference( + DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */, + OPPORTUNISTIC_QUOTA_UNKNOWN, + // 29 days from Apr. 2nd to May 1st + POLICY_SNOOZED /* policyWarning */, + POLICY_SNOOZED /* policyLimit */, + DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */, + 2_500_000 /* defaultResSetting */, + false /* roaming */); + + // Default global setting should be used: 12 - 7 = 5 + verify(mStatsManager, times(1)).registerUsageCallback( + any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(5)), any(), any()); + } + + @Test + public void testGetMultipathPreference_SettingChanged() { + testGetMultipathPreference( + DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */, + OPPORTUNISTIC_QUOTA_UNKNOWN, + WARNING_DISABLED, + LIMIT_DISABLED, + -1 /* defaultGlobalSetting */, + DataUnit.MEGABYTES.toBytes(10) /* defaultResSetting */, + false /* roaming */); + + verify(mStatsManager, times(1)).registerUsageCallback( + any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any()); + + // Update setting + setDefaultQuotaGlobalSetting(DataUnit.MEGABYTES.toBytes(14)); + mTracker.mSettingsObserver.onChange( + false, Settings.Global.getUriFor(NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES)); + + // Callback must have been re-registered with new setting + verify(mStatsManager, times(1)).unregisterUsageCallback(any()); + verify(mStatsManager, times(1)).registerUsageCallback( + any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any()); + } + + @Test + public void testGetMultipathPreference_ResourceChanged() { + testGetMultipathPreference( + DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */, + OPPORTUNISTIC_QUOTA_UNKNOWN, + WARNING_DISABLED, + LIMIT_DISABLED, + -1 /* defaultGlobalSetting */, + DataUnit.MEGABYTES.toBytes(14) /* defaultResSetting */, + false /* roaming */); + + verify(mStatsManager, times(1)).registerUsageCallback( + any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any()); + + when(mResources.getInteger(R.integer.config_networkDefaultDailyMultipathQuotaBytes)) + .thenReturn((int) DataUnit.MEGABYTES.toBytes(16)); + + final BroadcastReceiver configChangeReceiver = mConfigChangeReceiverCaptor.getValue(); + assertNotNull(configChangeReceiver); + configChangeReceiver.onReceive(mContext, new Intent()); + + // Uses the new setting (16 - 2 = 14MB) + verify(mStatsManager, times(1)).registerUsageCallback( + any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(14)), any(), any()); + } +} diff --git a/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java b/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java new file mode 100644 index 0000000000..9b2a638f8b --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java @@ -0,0 +1,555 @@ +/* + * 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.NetworkCapabilities.TRANSPORT_CELLULAR; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.net.ConnectivityManager; +import android.net.IDnsResolver; +import android.net.INetd; +import android.net.InterfaceConfigurationParcel; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.NetworkAgentConfig; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.os.Handler; +import android.os.test.TestLooper; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.ConnectivityService; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class Nat464XlatTest { + + static final String BASE_IFACE = "test0"; + static final String STACKED_IFACE = "v4-test0"; + static final LinkAddress V6ADDR = new LinkAddress("2001:db8:1::f00/64"); + static final LinkAddress ADDR = new LinkAddress("192.0.2.5/29"); + static final String NAT64_PREFIX = "64:ff9b::/96"; + static final String OTHER_NAT64_PREFIX = "2001:db8:0:64::/96"; + static final int NETID = 42; + + @Mock ConnectivityService mConnectivity; + @Mock IDnsResolver mDnsResolver; + @Mock INetd mNetd; + @Mock NetworkAgentInfo mNai; + + TestLooper mLooper; + Handler mHandler; + NetworkAgentConfig mAgentConfig = new NetworkAgentConfig(); + + Nat464Xlat makeNat464Xlat(boolean isCellular464XlatEnabled) { + return new Nat464Xlat(mNai, mNetd, mDnsResolver, new ConnectivityService.Dependencies()) { + @Override protected int getNetId() { + return NETID; + } + + @Override protected boolean isCellular464XlatEnabled() { + return isCellular464XlatEnabled; + } + }; + } + + private void markNetworkConnected() { + mNai.networkInfo.setDetailedState(NetworkInfo.DetailedState.CONNECTED, "", ""); + } + + private void markNetworkDisconnected() { + mNai.networkInfo.setDetailedState(NetworkInfo.DetailedState.DISCONNECTED, "", ""); + } + + @Before + public void setUp() throws Exception { + mLooper = new TestLooper(); + mHandler = new Handler(mLooper.getLooper()); + + MockitoAnnotations.initMocks(this); + + mNai.linkProperties = new LinkProperties(); + mNai.linkProperties.setInterfaceName(BASE_IFACE); + mNai.networkInfo = new NetworkInfo(null); + mNai.networkInfo.setType(ConnectivityManager.TYPE_WIFI); + mNai.networkCapabilities = new NetworkCapabilities(); + markNetworkConnected(); + when(mNai.connService()).thenReturn(mConnectivity); + when(mNai.netAgentConfig()).thenReturn(mAgentConfig); + when(mNai.handler()).thenReturn(mHandler); + final InterfaceConfigurationParcel mConfig = new InterfaceConfigurationParcel(); + when(mNetd.interfaceGetCfg(eq(STACKED_IFACE))).thenReturn(mConfig); + mConfig.ipv4Addr = ADDR.getAddress().getHostAddress(); + mConfig.prefixLength = ADDR.getPrefixLength(); + } + + private void assertRequiresClat(boolean expected, NetworkAgentInfo nai) { + Nat464Xlat nat = makeNat464Xlat(true); + String msg = String.format("requiresClat expected %b for type=%d state=%s skip=%b " + + "nat64Prefix=%s addresses=%s", expected, nai.networkInfo.getType(), + nai.networkInfo.getDetailedState(), + mAgentConfig.skip464xlat, nai.linkProperties.getNat64Prefix(), + nai.linkProperties.getLinkAddresses()); + assertEquals(msg, expected, nat.requiresClat(nai)); + } + + private void assertShouldStartClat(boolean expected, NetworkAgentInfo nai) { + Nat464Xlat nat = makeNat464Xlat(true); + String msg = String.format("shouldStartClat expected %b for type=%d state=%s skip=%b " + + "nat64Prefix=%s addresses=%s", expected, nai.networkInfo.getType(), + nai.networkInfo.getDetailedState(), + mAgentConfig.skip464xlat, nai.linkProperties.getNat64Prefix(), + nai.linkProperties.getLinkAddresses()); + assertEquals(msg, expected, nat.shouldStartClat(nai)); + } + + @Test + public void testRequiresClat() throws Exception { + final int[] supportedTypes = { + ConnectivityManager.TYPE_MOBILE, + ConnectivityManager.TYPE_WIFI, + ConnectivityManager.TYPE_ETHERNET, + }; + + // NetworkInfo doesn't allow setting the State directly, but rather + // requires setting DetailedState in order set State as a side-effect. + final NetworkInfo.DetailedState[] supportedDetailedStates = { + NetworkInfo.DetailedState.CONNECTED, + NetworkInfo.DetailedState.SUSPENDED, + }; + + LinkProperties oldLp = new LinkProperties(mNai.linkProperties); + for (int type : supportedTypes) { + mNai.networkInfo.setType(type); + for (NetworkInfo.DetailedState state : supportedDetailedStates) { + mNai.networkInfo.setDetailedState(state, "reason", "extraInfo"); + + mNai.linkProperties.setNat64Prefix(new IpPrefix(OTHER_NAT64_PREFIX)); + assertRequiresClat(false, mNai); + assertShouldStartClat(false, mNai); + + mNai.linkProperties.addLinkAddress(new LinkAddress("fc00::1/64")); + assertRequiresClat(false, mNai); + assertShouldStartClat(false, mNai); + + mNai.linkProperties.addLinkAddress(new LinkAddress("2001:db8::1/64")); + assertRequiresClat(true, mNai); + assertShouldStartClat(true, mNai); + + mAgentConfig.skip464xlat = true; + assertRequiresClat(false, mNai); + assertShouldStartClat(false, mNai); + + mAgentConfig.skip464xlat = false; + assertRequiresClat(true, mNai); + assertShouldStartClat(true, mNai); + + mNai.linkProperties.addLinkAddress(new LinkAddress("192.0.2.2/24")); + assertRequiresClat(false, mNai); + assertShouldStartClat(false, mNai); + + mNai.linkProperties.removeLinkAddress(new LinkAddress("192.0.2.2/24")); + assertRequiresClat(true, mNai); + assertShouldStartClat(true, mNai); + + mNai.linkProperties.setNat64Prefix(null); + assertRequiresClat(true, mNai); + assertShouldStartClat(false, mNai); + + mNai.linkProperties = new LinkProperties(oldLp); + } + } + } + + private void makeClatUnnecessary(boolean dueToDisconnect) { + if (dueToDisconnect) { + markNetworkDisconnected(); + } else { + mNai.linkProperties.addLinkAddress(ADDR); + } + } + + private void checkNormalStartAndStop(boolean dueToDisconnect) throws Exception { + Nat464Xlat nat = makeNat464Xlat(true); + ArgumentCaptor c = ArgumentCaptor.forClass(LinkProperties.class); + + mNai.linkProperties.addLinkAddress(V6ADDR); + + nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX)); + + // Start clat. + nat.start(); + + verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX)); + + // Stacked interface up notification arrives. + nat.interfaceLinkStateChanged(STACKED_IFACE, true); + mLooper.dispatchNext(); + + verify(mNetd).interfaceGetCfg(eq(STACKED_IFACE)); + verify(mConnectivity).handleUpdateLinkProperties(eq(mNai), c.capture()); + assertFalse(c.getValue().getStackedLinks().isEmpty()); + assertTrue(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE)); + assertRunning(nat); + + // Stop clat (Network disconnects, IPv4 addr appears, ...). + makeClatUnnecessary(dueToDisconnect); + nat.stop(); + + verify(mNetd).clatdStop(eq(BASE_IFACE)); + verify(mConnectivity, times(2)).handleUpdateLinkProperties(eq(mNai), c.capture()); + assertTrue(c.getValue().getStackedLinks().isEmpty()); + assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE)); + verify(mDnsResolver).stopPrefix64Discovery(eq(NETID)); + assertIdle(nat); + + // Stacked interface removed notification arrives and is ignored. + nat.interfaceRemoved(STACKED_IFACE); + mLooper.dispatchNext(); + + verifyNoMoreInteractions(mNetd, mConnectivity); + } + + @Test + public void testNormalStartAndStopDueToDisconnect() throws Exception { + checkNormalStartAndStop(true); + } + + @Test + public void testNormalStartAndStopDueToIpv4Addr() throws Exception { + checkNormalStartAndStop(false); + } + + private void checkStartStopStart(boolean interfaceRemovedFirst) throws Exception { + Nat464Xlat nat = makeNat464Xlat(true); + ArgumentCaptor c = ArgumentCaptor.forClass(LinkProperties.class); + InOrder inOrder = inOrder(mNetd, mConnectivity); + + mNai.linkProperties.addLinkAddress(V6ADDR); + + nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX)); + + nat.start(); + + inOrder.verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX)); + + // Stacked interface up notification arrives. + nat.interfaceLinkStateChanged(STACKED_IFACE, true); + mLooper.dispatchNext(); + + inOrder.verify(mConnectivity).handleUpdateLinkProperties(eq(mNai), c.capture()); + assertFalse(c.getValue().getStackedLinks().isEmpty()); + assertTrue(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE)); + assertRunning(nat); + + // ConnectivityService stops clat (Network disconnects, IPv4 addr appears, ...). + nat.stop(); + + inOrder.verify(mNetd).clatdStop(eq(BASE_IFACE)); + + inOrder.verify(mConnectivity, times(1)).handleUpdateLinkProperties(eq(mNai), c.capture()); + assertTrue(c.getValue().getStackedLinks().isEmpty()); + assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE)); + assertIdle(nat); + + if (interfaceRemovedFirst) { + // Stacked interface removed notification arrives and is ignored. + nat.interfaceRemoved(STACKED_IFACE); + mLooper.dispatchNext(); + nat.interfaceLinkStateChanged(STACKED_IFACE, false); + mLooper.dispatchNext(); + } + + assertTrue(c.getValue().getStackedLinks().isEmpty()); + assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE)); + assertIdle(nat); + inOrder.verifyNoMoreInteractions(); + + nat.start(); + + inOrder.verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX)); + + if (!interfaceRemovedFirst) { + // Stacked interface removed notification arrives and is ignored. + nat.interfaceRemoved(STACKED_IFACE); + mLooper.dispatchNext(); + nat.interfaceLinkStateChanged(STACKED_IFACE, false); + mLooper.dispatchNext(); + } + + // Stacked interface up notification arrives. + nat.interfaceLinkStateChanged(STACKED_IFACE, true); + mLooper.dispatchNext(); + + inOrder.verify(mConnectivity).handleUpdateLinkProperties(eq(mNai), c.capture()); + assertFalse(c.getValue().getStackedLinks().isEmpty()); + assertTrue(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE)); + assertRunning(nat); + + // ConnectivityService stops clat again. + nat.stop(); + + inOrder.verify(mNetd).clatdStop(eq(BASE_IFACE)); + + inOrder.verify(mConnectivity, times(1)).handleUpdateLinkProperties(eq(mNai), c.capture()); + assertTrue(c.getValue().getStackedLinks().isEmpty()); + assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE)); + assertIdle(nat); + + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testStartStopStart() throws Exception { + checkStartStopStart(true); + } + + @Test + public void testStartStopStartBeforeInterfaceRemoved() throws Exception { + checkStartStopStart(false); + } + + @Test + public void testClatdCrashWhileRunning() throws Exception { + Nat464Xlat nat = makeNat464Xlat(true); + ArgumentCaptor c = ArgumentCaptor.forClass(LinkProperties.class); + + nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX)); + + nat.start(); + + verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX)); + + // Stacked interface up notification arrives. + nat.interfaceLinkStateChanged(STACKED_IFACE, true); + mLooper.dispatchNext(); + + verify(mNetd).interfaceGetCfg(eq(STACKED_IFACE)); + verify(mConnectivity, times(1)).handleUpdateLinkProperties(eq(mNai), c.capture()); + assertFalse(c.getValue().getStackedLinks().isEmpty()); + assertTrue(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE)); + assertRunning(nat); + + // Stacked interface removed notification arrives (clatd crashed, ...). + nat.interfaceRemoved(STACKED_IFACE); + mLooper.dispatchNext(); + + verify(mNetd).clatdStop(eq(BASE_IFACE)); + verify(mConnectivity, times(2)).handleUpdateLinkProperties(eq(mNai), c.capture()); + verify(mDnsResolver).stopPrefix64Discovery(eq(NETID)); + assertTrue(c.getValue().getStackedLinks().isEmpty()); + assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE)); + assertIdle(nat); + + // ConnectivityService stops clat: no-op. + nat.stop(); + + verifyNoMoreInteractions(mNetd, mConnectivity); + } + + private void checkStopBeforeClatdStarts(boolean dueToDisconnect) throws Exception { + Nat464Xlat nat = makeNat464Xlat(true); + + mNai.linkProperties.addLinkAddress(new LinkAddress("2001:db8::1/64")); + + nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX)); + + nat.start(); + + verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX)); + + // ConnectivityService immediately stops clat (Network disconnects, IPv4 addr appears, ...) + makeClatUnnecessary(dueToDisconnect); + nat.stop(); + + verify(mNetd).clatdStop(eq(BASE_IFACE)); + verify(mDnsResolver).stopPrefix64Discovery(eq(NETID)); + assertIdle(nat); + + // In-flight interface up notification arrives: no-op + nat.interfaceLinkStateChanged(STACKED_IFACE, true); + mLooper.dispatchNext(); + + // Interface removed notification arrives after stopClatd() takes effect: no-op. + nat.interfaceRemoved(STACKED_IFACE); + mLooper.dispatchNext(); + + assertIdle(nat); + + verifyNoMoreInteractions(mNetd, mConnectivity); + } + + @Test + public void testStopDueToDisconnectBeforeClatdStarts() throws Exception { + checkStopBeforeClatdStarts(true); + } + + @Test + public void testStopDueToIpv4AddrBeforeClatdStarts() throws Exception { + checkStopBeforeClatdStarts(false); + } + + private void checkStopAndClatdNeverStarts(boolean dueToDisconnect) throws Exception { + Nat464Xlat nat = makeNat464Xlat(true); + + mNai.linkProperties.addLinkAddress(new LinkAddress("2001:db8::1/64")); + + nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX)); + + nat.start(); + + verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX)); + + // ConnectivityService immediately stops clat (Network disconnects, IPv4 addr appears, ...) + makeClatUnnecessary(dueToDisconnect); + nat.stop(); + + verify(mNetd).clatdStop(eq(BASE_IFACE)); + verify(mDnsResolver).stopPrefix64Discovery(eq(NETID)); + assertIdle(nat); + + verifyNoMoreInteractions(mNetd, mConnectivity); + } + + @Test + public void testStopDueToDisconnectAndClatdNeverStarts() throws Exception { + checkStopAndClatdNeverStarts(true); + } + + @Test + public void testStopDueToIpv4AddressAndClatdNeverStarts() throws Exception { + checkStopAndClatdNeverStarts(false); + } + + @Test + public void testNat64PrefixPreference() throws Exception { + final IpPrefix prefixFromDns = new IpPrefix(NAT64_PREFIX); + final IpPrefix prefixFromRa = new IpPrefix(OTHER_NAT64_PREFIX); + + Nat464Xlat nat = makeNat464Xlat(true); + + final LinkProperties emptyLp = new LinkProperties(); + LinkProperties fixedupLp; + + fixedupLp = new LinkProperties(); + nat.setNat64PrefixFromDns(prefixFromDns); + nat.fixupLinkProperties(emptyLp, fixedupLp); + assertEquals(prefixFromDns, fixedupLp.getNat64Prefix()); + + fixedupLp = new LinkProperties(); + nat.setNat64PrefixFromRa(prefixFromRa); + nat.fixupLinkProperties(emptyLp, fixedupLp); + assertEquals(prefixFromRa, fixedupLp.getNat64Prefix()); + + fixedupLp = new LinkProperties(); + nat.setNat64PrefixFromRa(null); + nat.fixupLinkProperties(emptyLp, fixedupLp); + assertEquals(prefixFromDns, fixedupLp.getNat64Prefix()); + + fixedupLp = new LinkProperties(); + nat.setNat64PrefixFromRa(prefixFromRa); + nat.fixupLinkProperties(emptyLp, fixedupLp); + assertEquals(prefixFromRa, fixedupLp.getNat64Prefix()); + + fixedupLp = new LinkProperties(); + nat.setNat64PrefixFromDns(null); + nat.fixupLinkProperties(emptyLp, fixedupLp); + assertEquals(prefixFromRa, fixedupLp.getNat64Prefix()); + + fixedupLp = new LinkProperties(); + nat.setNat64PrefixFromRa(null); + nat.fixupLinkProperties(emptyLp, fixedupLp); + assertEquals(null, fixedupLp.getNat64Prefix()); + } + + private void checkClatDisabledOnCellular(boolean onCellular) throws Exception { + // Disable 464xlat on cellular networks. + Nat464Xlat nat = makeNat464Xlat(false); + mNai.linkProperties.addLinkAddress(V6ADDR); + mNai.networkCapabilities.setTransportType(TRANSPORT_CELLULAR, onCellular); + nat.update(); + + final IpPrefix nat64Prefix = new IpPrefix(NAT64_PREFIX); + if (onCellular) { + // Prefix discovery is never started. + verify(mDnsResolver, never()).startPrefix64Discovery(eq(NETID)); + assertIdle(nat); + + // If a NAT64 prefix comes in from an RA, clat is not started either. + mNai.linkProperties.setNat64Prefix(nat64Prefix); + nat.setNat64PrefixFromRa(nat64Prefix); + nat.update(); + verify(mNetd, never()).clatdStart(anyString(), anyString()); + assertIdle(nat); + } else { + // Prefix discovery is started. + verify(mDnsResolver).startPrefix64Discovery(eq(NETID)); + assertIdle(nat); + + // If a NAT64 prefix comes in from an RA, clat is started. + mNai.linkProperties.setNat64Prefix(nat64Prefix); + nat.setNat64PrefixFromRa(nat64Prefix); + nat.update(); + verify(mNetd).clatdStart(BASE_IFACE, NAT64_PREFIX); + assertStarting(nat); + } + } + + @Test + public void testClatDisabledOnCellular() throws Exception { + checkClatDisabledOnCellular(true); + } + + @Test + public void testClatDisabledOnNonCellular() throws Exception { + checkClatDisabledOnCellular(false); + } + + static void assertIdle(Nat464Xlat nat) { + assertTrue("Nat464Xlat was not IDLE", !nat.isStarted()); + } + + static void assertStarting(Nat464Xlat nat) { + assertTrue("Nat464Xlat was not STARTING", nat.isStarting()); + } + + static void assertRunning(Nat464Xlat nat) { + assertTrue("Nat464Xlat was not RUNNING", nat.isRunning()); + } +} diff --git a/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java b/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java new file mode 100644 index 0000000000..50aaaee244 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java @@ -0,0 +1,554 @@ +/* + * Copyright (C) 2016, 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.metrics.INetdEventListener.EVENT_GETADDRINFO; +import static android.net.metrics.INetdEventListener.EVENT_GETHOSTBYNAME; + +import static com.android.testutils.MiscAsserts.assertStringContains; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.system.OsConstants; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Base64; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent; +import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityLog; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; + +import java.io.FileOutputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetdEventListenerServiceTest { + private static final String EXAMPLE_IPV4 = "192.0.2.1"; + private static final String EXAMPLE_IPV6 = "2001:db8:1200::2:1"; + + private static final byte[] MAC_ADDR = + {(byte)0x84, (byte)0xc9, (byte)0xb2, (byte)0x6a, (byte)0xed, (byte)0x4b}; + + NetdEventListenerService mService; + ConnectivityManager mCm; + private static final NetworkCapabilities CAPABILITIES_WIFI = new NetworkCapabilities.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .build(); + private static final NetworkCapabilities CAPABILITIES_CELL = new NetworkCapabilities.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .build(); + + @Before + public void setUp() { + mCm = mock(ConnectivityManager.class); + mService = new NetdEventListenerService(mCm); + } + + @Test + public void testWakeupEventLogging() throws Exception { + final int BUFFER_LENGTH = NetdEventListenerService.WAKEUP_EVENT_BUFFER_LENGTH; + final long now = System.currentTimeMillis(); + final String iface = "wlan0"; + final byte[] mac = MAC_ADDR; + final String srcIp = "192.168.2.1"; + final String dstIp = "192.168.2.23"; + final String srcIp6 = "2001:db8:4:fd00:a585:13d1:6a23:4fb4"; + final String dstIp6 = "2001:db8:4006:807::200a"; + final int sport = 2356; + final int dport = 13489; + + final int v4 = 0x800; + final int v6 = 0x86dd; + final int tcp = 6; + final int udp = 17; + final int icmp6 = 58; + + // Baseline without any event + String[] baseline = listNetdEvent(); + + int[] uids = {10001, 10002, 10004, 1000, 10052, 10023, 10002, 10123, 10004}; + wakeupEvent(iface, uids[0], v4, tcp, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent(iface, uids[1], v6, udp, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent(iface, uids[2], v6, udp, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent(iface, uids[3], v4, icmp6, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent(iface, uids[4], v6, tcp, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent(iface, uids[5], v4, tcp, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent(iface, uids[6], v6, udp, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent(iface, uids[7], v6, tcp, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent(iface, uids[8], v6, udp, mac, srcIp6, dstIp6, sport, dport, now); + + String[] events2 = remove(listNetdEvent(), baseline); + int expectedLength2 = uids.length + 1; // +1 for the WakeupStats line + assertEquals(expectedLength2, events2.length); + assertStringContains(events2[0], "WakeupStats"); + assertStringContains(events2[0], "wlan0"); + assertStringContains(events2[0], "0x800"); + assertStringContains(events2[0], "0x86dd"); + for (int i = 0; i < uids.length; i++) { + String got = events2[i+1]; + assertStringContains(got, "WakeupEvent"); + assertStringContains(got, "wlan0"); + assertStringContains(got, "uid: " + uids[i]); + } + + int uid = 20000; + for (int i = 0; i < BUFFER_LENGTH * 2; i++) { + long ts = now + 10; + wakeupEvent(iface, uid, 0x800, 6, mac, srcIp, dstIp, 23, 24, ts); + } + + String[] events3 = remove(listNetdEvent(), baseline); + int expectedLength3 = BUFFER_LENGTH + 1; // +1 for the WakeupStats line + assertEquals(expectedLength3, events3.length); + assertStringContains(events2[0], "WakeupStats"); + assertStringContains(events2[0], "wlan0"); + for (int i = 1; i < expectedLength3; i++) { + String got = events3[i]; + assertStringContains(got, "WakeupEvent"); + assertStringContains(got, "wlan0"); + assertStringContains(got, "uid: " + uid); + } + + uid = 45678; + wakeupEvent(iface, uid, 0x800, 6, mac, srcIp, dstIp, 23, 24, now); + + String[] events4 = remove(listNetdEvent(), baseline); + String lastEvent = events4[events4.length - 1]; + assertStringContains(lastEvent, "WakeupEvent"); + assertStringContains(lastEvent, "wlan0"); + assertStringContains(lastEvent, "uid: " + uid); + } + + @Test + public void testWakeupStatsLogging() throws Exception { + final byte[] mac = MAC_ADDR; + final String srcIp = "192.168.2.1"; + final String dstIp = "192.168.2.23"; + final String srcIp6 = "2401:fa00:4:fd00:a585:13d1:6a23:4fb4"; + final String dstIp6 = "2404:6800:4006:807::200a"; + final int sport = 2356; + final int dport = 13489; + final long now = 1001L; + + final int v4 = 0x800; + final int v6 = 0x86dd; + final int tcp = 6; + final int udp = 17; + final int icmp6 = 58; + + wakeupEvent("wlan0", 1000, v4, tcp, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent("rmnet0", 10123, v4, tcp, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent("wlan0", 1000, v4, udp, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent("rmnet0", 10008, v4, tcp, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent("wlan0", -1, v6, icmp6, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent("wlan0", 10008, v4, tcp, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent("rmnet0", 1000, v4, tcp, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent("wlan0", 10004, v4, udp, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent("wlan0", 1000, v6, tcp, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent("wlan0", 0, v6, udp, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent("wlan0", -1, v6, icmp6, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent("rmnet0", 10052, v4, tcp, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent("wlan0", 0, v6, udp, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent("rmnet0", 1000, v6, tcp, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent("wlan0", 1010, v4, udp, mac, srcIp, dstIp, sport, dport, now); + + String got = flushStatistics(); + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 2", + " network_id: 0", + " time_ms: 0", + " transports: 0", + " wakeup_stats <", + " application_wakeups: 3", + " duration_sec: 0", + " ethertype_counts <", + " key: 2048", + " value: 4", + " >", + " ethertype_counts <", + " key: 34525", + " value: 1", + " >", + " ip_next_header_counts <", + " key: 6", + " value: 5", + " >", + " l2_broadcast_count: 0", + " l2_multicast_count: 0", + " l2_unicast_count: 5", + " no_uid_wakeups: 0", + " non_application_wakeups: 0", + " root_wakeups: 0", + " system_wakeups: 2", + " total_wakeups: 5", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 0", + " time_ms: 0", + " transports: 0", + " wakeup_stats <", + " application_wakeups: 2", + " duration_sec: 0", + " ethertype_counts <", + " key: 2048", + " value: 5", + " >", + " ethertype_counts <", + " key: 34525", + " value: 5", + " >", + " ip_next_header_counts <", + " key: 6", + " value: 3", + " >", + " ip_next_header_counts <", + " key: 17", + " value: 5", + " >", + " ip_next_header_counts <", + " key: 58", + " value: 2", + " >", + " l2_broadcast_count: 0", + " l2_multicast_count: 0", + " l2_unicast_count: 10", + " no_uid_wakeups: 2", + " non_application_wakeups: 1", + " root_wakeups: 2", + " system_wakeups: 3", + " total_wakeups: 10", + " >", + ">", + "version: 2\n"); + assertEquals(want, got); + } + + @Test + public void testDnsLogging() throws Exception { + asyncDump(100); + + dnsEvent(100, EVENT_GETADDRINFO, 0, 3456); + dnsEvent(100, EVENT_GETADDRINFO, 0, 267); + dnsEvent(100, EVENT_GETHOSTBYNAME, 22, 1230); + dnsEvent(100, EVENT_GETADDRINFO, 3, 45); + dnsEvent(100, EVENT_GETADDRINFO, 1, 2111); + dnsEvent(100, EVENT_GETADDRINFO, 0, 450); + dnsEvent(100, EVENT_GETHOSTBYNAME, 200, 638); + dnsEvent(100, EVENT_GETHOSTBYNAME, 178, 1300); + dnsEvent(101, EVENT_GETADDRINFO, 0, 56); + dnsEvent(101, EVENT_GETADDRINFO, 0, 78); + dnsEvent(101, EVENT_GETADDRINFO, 0, 14); + dnsEvent(101, EVENT_GETHOSTBYNAME, 0, 56); + dnsEvent(101, EVENT_GETADDRINFO, 0, 78); + dnsEvent(101, EVENT_GETADDRINFO, 0, 14); + + String got = flushStatistics(); + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 100", + " time_ms: 0", + " transports: 2", + " dns_lookup_batch <", + " event_types: 1", + " event_types: 1", + " event_types: 2", + " event_types: 1", + " event_types: 1", + " event_types: 1", + " event_types: 2", + " event_types: 2", + " getaddrinfo_error_count: 0", + " getaddrinfo_query_count: 0", + " gethostbyname_error_count: 0", + " gethostbyname_query_count: 0", + " latencies_ms: 3456", + " latencies_ms: 267", + " latencies_ms: 1230", + " latencies_ms: 45", + " latencies_ms: 2111", + " latencies_ms: 450", + " latencies_ms: 638", + " latencies_ms: 1300", + " return_codes: 0", + " return_codes: 0", + " return_codes: 22", + " return_codes: 3", + " return_codes: 1", + " return_codes: 0", + " return_codes: 200", + " return_codes: 178", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 2", + " network_id: 101", + " time_ms: 0", + " transports: 1", + " dns_lookup_batch <", + " event_types: 1", + " event_types: 1", + " event_types: 1", + " event_types: 2", + " event_types: 1", + " event_types: 1", + " getaddrinfo_error_count: 0", + " getaddrinfo_query_count: 0", + " gethostbyname_error_count: 0", + " gethostbyname_query_count: 0", + " latencies_ms: 56", + " latencies_ms: 78", + " latencies_ms: 14", + " latencies_ms: 56", + " latencies_ms: 78", + " latencies_ms: 14", + " return_codes: 0", + " return_codes: 0", + " return_codes: 0", + " return_codes: 0", + " return_codes: 0", + " return_codes: 0", + " >", + ">", + "version: 2\n"); + assertEquals(want, got); + } + + @Test + public void testConnectLogging() throws Exception { + asyncDump(100); + + final int OK = 0; + Thread[] logActions = { + // ignored + connectEventAction(100, OsConstants.EALREADY, 0, EXAMPLE_IPV4), + connectEventAction(100, OsConstants.EALREADY, 0, EXAMPLE_IPV6), + connectEventAction(100, OsConstants.EINPROGRESS, 0, EXAMPLE_IPV4), + connectEventAction(101, OsConstants.EINPROGRESS, 0, EXAMPLE_IPV6), + connectEventAction(101, OsConstants.EINPROGRESS, 0, EXAMPLE_IPV6), + // valid latencies + connectEventAction(100, OK, 110, EXAMPLE_IPV4), + connectEventAction(100, OK, 23, EXAMPLE_IPV4), + connectEventAction(100, OK, 45, EXAMPLE_IPV4), + connectEventAction(101, OK, 56, EXAMPLE_IPV4), + connectEventAction(101, OK, 523, EXAMPLE_IPV6), + connectEventAction(101, OK, 214, EXAMPLE_IPV6), + connectEventAction(101, OK, 67, EXAMPLE_IPV6), + // errors + connectEventAction(100, OsConstants.EPERM, 0, EXAMPLE_IPV4), + connectEventAction(101, OsConstants.EPERM, 0, EXAMPLE_IPV4), + connectEventAction(100, OsConstants.EAGAIN, 0, EXAMPLE_IPV4), + connectEventAction(100, OsConstants.EACCES, 0, EXAMPLE_IPV4), + connectEventAction(101, OsConstants.EACCES, 0, EXAMPLE_IPV4), + connectEventAction(101, OsConstants.EACCES, 0, EXAMPLE_IPV6), + connectEventAction(100, OsConstants.EADDRINUSE, 0, EXAMPLE_IPV4), + connectEventAction(101, OsConstants.ETIMEDOUT, 0, EXAMPLE_IPV4), + connectEventAction(100, OsConstants.ETIMEDOUT, 0, EXAMPLE_IPV6), + connectEventAction(100, OsConstants.ETIMEDOUT, 0, EXAMPLE_IPV6), + connectEventAction(101, OsConstants.ECONNREFUSED, 0, EXAMPLE_IPV4), + }; + + for (Thread t : logActions) { + t.start(); + } + for (Thread t : logActions) { + t.join(); + } + + String got = flushStatistics(); + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 100", + " time_ms: 0", + " transports: 2", + " connect_statistics <", + " connect_blocking_count: 3", + " connect_count: 6", + " errnos_counters <", + " key: 1", + " value: 1", + " >", + " errnos_counters <", + " key: 11", + " value: 1", + " >", + " errnos_counters <", + " key: 13", + " value: 1", + " >", + " errnos_counters <", + " key: 98", + " value: 1", + " >", + " errnos_counters <", + " key: 110", + " value: 2", + " >", + " ipv6_addr_count: 1", + " latencies_ms: 23", + " latencies_ms: 45", + " latencies_ms: 110", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 2", + " network_id: 101", + " time_ms: 0", + " transports: 1", + " connect_statistics <", + " connect_blocking_count: 4", + " connect_count: 6", + " errnos_counters <", + " key: 1", + " value: 1", + " >", + " errnos_counters <", + " key: 13", + " value: 2", + " >", + " errnos_counters <", + " key: 110", + " value: 1", + " >", + " errnos_counters <", + " key: 111", + " value: 1", + " >", + " ipv6_addr_count: 5", + " latencies_ms: 56", + " latencies_ms: 67", + " latencies_ms: 214", + " latencies_ms: 523", + " >", + ">", + "version: 2\n"); + assertEquals(want, got); + } + + private void setCapabilities(int netId) { + final ArgumentCaptor networkCallback = + ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class); + verify(mCm).registerNetworkCallback(any(), networkCallback.capture()); + networkCallback.getValue().onCapabilitiesChanged(new Network(netId), + netId == 100 ? CAPABILITIES_WIFI : CAPABILITIES_CELL); + } + + Thread connectEventAction(int netId, int error, int latencyMs, String ipAddr) { + setCapabilities(netId); + return new Thread(() -> { + try { + mService.onConnectEvent(netId, error, latencyMs, ipAddr, 80, 1); + } catch (Exception e) { + fail(e.toString()); + } + }); + } + + void dnsEvent(int netId, int type, int result, int latency) throws Exception { + setCapabilities(netId); + mService.onDnsEvent(netId, type, result, latency, "", null, 0, 0); + } + + void wakeupEvent(String iface, int uid, int ether, int ip, byte[] mac, String srcIp, + String dstIp, int sport, int dport, long now) throws Exception { + String prefix = NetdEventListenerService.WAKEUP_EVENT_IFACE_PREFIX + iface; + mService.onWakeupEvent(prefix, uid, ether, ip, mac, srcIp, dstIp, sport, dport, now); + } + + void asyncDump(long durationMs) throws Exception { + final long stop = System.currentTimeMillis() + durationMs; + final PrintWriter pw = new PrintWriter(new FileOutputStream("/dev/null")); + new Thread(() -> { + while (System.currentTimeMillis() < stop) { + mService.list(pw); + } + }).start(); + } + + // TODO: instead of comparing textpb to textpb, parse textpb and compare proto to proto. + String flushStatistics() throws Exception { + IpConnectivityMetrics metricsService = + new IpConnectivityMetrics(mock(Context.class), (ctx) -> 2000); + metricsService.mNetdListener = mService; + + StringWriter buffer = new StringWriter(); + PrintWriter writer = new PrintWriter(buffer); + metricsService.impl.dump(null, writer, new String[]{"flush"}); + byte[] bytes = Base64.decode(buffer.toString(), Base64.DEFAULT); + IpConnectivityLog log = IpConnectivityLog.parseFrom(bytes); + for (IpConnectivityEvent ev : log.events) { + if (ev.getConnectStatistics() == null) { + continue; + } + // Sort repeated fields of connect() events arriving in non-deterministic order. + Arrays.sort(ev.getConnectStatistics().latenciesMs); + Arrays.sort(ev.getConnectStatistics().errnosCounters, + Comparator.comparingInt((p) -> p.key)); + } + return log.toString(); + } + + String[] listNetdEvent() throws Exception { + StringWriter buffer = new StringWriter(); + PrintWriter writer = new PrintWriter(buffer); + mService.list(writer); + return buffer.toString().split("\\n"); + } + + static T[] remove(T[] array, T[] filtered) { + List c = Arrays.asList(filtered); + int next = 0; + for (int i = 0; i < array.length; i++) { + if (c.contains(array[i])) { + continue; + } + array[next++] = array[i]; + } + return Arrays.copyOf(array, next); + } +} diff --git a/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java new file mode 100644 index 0000000000..3adf08c199 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2016 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 com.android.server.connectivity.NetworkNotificationManager.NotificationType.*; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Notification; +import android.app.NotificationManager; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.net.ConnectivityResources; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.os.UserHandle; +import android.telephony.TelephonyManager; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.connectivity.resources.R; +import com.android.server.connectivity.NetworkNotificationManager.NotificationType; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.AdditionalAnswers; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkNotificationManagerTest { + + private static final String TEST_SSID = "Test SSID"; + private static final String TEST_EXTRA_INFO = "extra"; + static final NetworkCapabilities CELL_CAPABILITIES = new NetworkCapabilities(); + static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities(); + static final NetworkCapabilities VPN_CAPABILITIES = new NetworkCapabilities(); + static { + CELL_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR); + CELL_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + + WIFI_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_WIFI); + WIFI_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + WIFI_CAPABILITIES.setSSID(TEST_SSID); + + // Set the underyling network to wifi. + VPN_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_WIFI); + VPN_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_VPN); + VPN_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + VPN_CAPABILITIES.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN); + } + + @Mock Context mCtx; + @Mock Resources mResources; + @Mock PackageManager mPm; + @Mock TelephonyManager mTelephonyManager; + @Mock NotificationManager mNotificationManager; + @Mock NetworkAgentInfo mWifiNai; + @Mock NetworkAgentInfo mCellNai; + @Mock NetworkAgentInfo mVpnNai; + @Mock NetworkInfo mNetworkInfo; + ArgumentCaptor mCaptor; + + NetworkNotificationManager mManager; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mCaptor = ArgumentCaptor.forClass(Notification.class); + mWifiNai.networkCapabilities = WIFI_CAPABILITIES; + mWifiNai.networkInfo = mNetworkInfo; + mCellNai.networkCapabilities = CELL_CAPABILITIES; + mCellNai.networkInfo = mNetworkInfo; + mVpnNai.networkCapabilities = VPN_CAPABILITIES; + mVpnNai.networkInfo = mNetworkInfo; + doReturn(true).when(mVpnNai).isVPN(); + when(mCtx.getResources()).thenReturn(mResources); + when(mCtx.getPackageManager()).thenReturn(mPm); + when(mCtx.getApplicationInfo()).thenReturn(new ApplicationInfo()); + final Context asUserCtx = mock(Context.class, AdditionalAnswers.delegatesTo(mCtx)); + doReturn(UserHandle.ALL).when(asUserCtx).getUser(); + when(mCtx.createContextAsUser(eq(UserHandle.ALL), anyInt())).thenReturn(asUserCtx); + when(mCtx.getSystemService(eq(Context.NOTIFICATION_SERVICE))) + .thenReturn(mNotificationManager); + when(mNetworkInfo.getExtraInfo()).thenReturn(TEST_EXTRA_INFO); + ConnectivityResources.setResourcesContextForTest(mCtx); + when(mResources.getColor(anyInt(), any())).thenReturn(0xFF607D8B); + + // Come up with some credible-looking transport names. The actual values do not matter. + String[] transportNames = new String[NetworkCapabilities.MAX_TRANSPORT + 1]; + for (int transport = 0; transport <= NetworkCapabilities.MAX_TRANSPORT; transport++) { + transportNames[transport] = NetworkCapabilities.transportNameOf(transport); + } + when(mResources.getStringArray(R.array.network_switch_type_name)) + .thenReturn(transportNames); + + mManager = new NetworkNotificationManager(mCtx, mTelephonyManager); + } + + @After + public void tearDown() { + ConnectivityResources.setResourcesContextForTest(null); + } + + private void verifyTitleByNetwork(final int id, final NetworkAgentInfo nai, final int title) { + final String tag = NetworkNotificationManager.tagFor(id); + mManager.showNotification(id, PRIVATE_DNS_BROKEN, nai, null, null, true); + verify(mNotificationManager, times(1)) + .notify(eq(tag), eq(PRIVATE_DNS_BROKEN.eventId), any()); + final int transportType = NetworkNotificationManager.approximateTransportType(nai); + if (transportType == NetworkCapabilities.TRANSPORT_WIFI) { + verify(mResources, times(1)).getString(eq(title), eq(TEST_EXTRA_INFO)); + } else { + verify(mResources, times(1)).getString(title); + } + verify(mResources, times(1)).getString(eq(R.string.private_dns_broken_detailed)); + } + + @Test + public void testTitleOfPrivateDnsBroken() { + // Test the title of mobile data. + verifyTitleByNetwork(100, mCellNai, R.string.mobile_no_internet); + clearInvocations(mResources); + + // Test the title of wifi. + verifyTitleByNetwork(101, mWifiNai, R.string.wifi_no_internet); + clearInvocations(mResources); + + // Test the title of other networks. + verifyTitleByNetwork(102, mVpnNai, R.string.other_networks_no_internet); + clearInvocations(mResources); + } + + @Test + public void testNotificationsShownAndCleared() { + final int NETWORK_ID_BASE = 100; + List types = Arrays.asList(NotificationType.values()); + List ids = new ArrayList<>(types.size()); + for (int i = 0; i < types.size(); i++) { + ids.add(NETWORK_ID_BASE + i); + } + Collections.shuffle(ids); + Collections.shuffle(types); + + for (int i = 0; i < ids.size(); i++) { + mManager.showNotification(ids.get(i), types.get(i), mWifiNai, mCellNai, null, false); + } + + List idsToClear = new ArrayList<>(ids); + Collections.shuffle(idsToClear); + for (int i = 0; i < ids.size(); i++) { + mManager.clearNotification(idsToClear.get(i)); + } + + for (int i = 0; i < ids.size(); i++) { + final int id = ids.get(i); + final int eventId = types.get(i).eventId; + final String tag = NetworkNotificationManager.tagFor(id); + verify(mNotificationManager, times(1)).notify(eq(tag), eq(eventId), any()); + verify(mNotificationManager, times(1)).cancel(eq(tag), eq(eventId)); + } + } + + @Test + @Ignore + // Ignored because the code under test calls Log.wtf, which crashes the tests on eng builds. + // TODO: re-enable after fixing this (e.g., turn Log.wtf into exceptions that this test catches) + public void testNoInternetNotificationsNotShownForCellular() { + mManager.showNotification(100, NO_INTERNET, mCellNai, mWifiNai, null, false); + mManager.showNotification(101, LOST_INTERNET, mCellNai, mWifiNai, null, false); + + verify(mNotificationManager, never()).notify(any(), anyInt(), any()); + + mManager.showNotification(102, NO_INTERNET, mWifiNai, mCellNai, null, false); + + final int eventId = NO_INTERNET.eventId; + final String tag = NetworkNotificationManager.tagFor(102); + verify(mNotificationManager, times(1)).notify(eq(tag), eq(eventId), any()); + } + + @Test + public void testNotificationsNotShownIfNoInternetCapability() { + mWifiNai.networkCapabilities = new NetworkCapabilities(); + mWifiNai.networkCapabilities .addTransportType(NetworkCapabilities.TRANSPORT_WIFI); + mManager.showNotification(102, NO_INTERNET, mWifiNai, mCellNai, null, false); + mManager.showNotification(103, LOST_INTERNET, mWifiNai, mCellNai, null, false); + mManager.showNotification(104, NETWORK_SWITCH, mWifiNai, mCellNai, null, false); + + verify(mNotificationManager, never()).notify(any(), anyInt(), any()); + } + + @Test + public void testDuplicatedNotificationsNoInternetThenSignIn() { + final int id = 101; + final String tag = NetworkNotificationManager.tagFor(id); + + // Show first NO_INTERNET + mManager.showNotification(id, NO_INTERNET, mWifiNai, mCellNai, null, false); + verify(mNotificationManager, times(1)).notify(eq(tag), eq(NO_INTERNET.eventId), any()); + + // Captive portal detection triggers SIGN_IN a bit later, clearing the previous NO_INTERNET + mManager.showNotification(id, SIGN_IN, mWifiNai, mCellNai, null, false); + verify(mNotificationManager, times(1)).cancel(eq(tag), eq(NO_INTERNET.eventId)); + verify(mNotificationManager, times(1)).notify(eq(tag), eq(SIGN_IN.eventId), any()); + + // Network disconnects + mManager.clearNotification(id); + verify(mNotificationManager, times(1)).cancel(eq(tag), eq(SIGN_IN.eventId)); + } + + @Test + public void testDuplicatedNotificationsSignInThenNoInternet() { + final int id = 101; + final String tag = NetworkNotificationManager.tagFor(id); + + // Show first SIGN_IN + mManager.showNotification(id, SIGN_IN, mWifiNai, mCellNai, null, false); + verify(mNotificationManager, times(1)).notify(eq(tag), eq(SIGN_IN.eventId), any()); + reset(mNotificationManager); + + // NO_INTERNET arrives after, but is ignored. + mManager.showNotification(id, NO_INTERNET, mWifiNai, mCellNai, null, false); + verify(mNotificationManager, never()).cancel(any(), anyInt()); + verify(mNotificationManager, never()).notify(any(), anyInt(), any()); + + // Network disconnects + mManager.clearNotification(id); + verify(mNotificationManager, times(1)).cancel(eq(tag), eq(SIGN_IN.eventId)); + } + + @Test + public void testClearNotificationByType() { + final int id = 101; + final String tag = NetworkNotificationManager.tagFor(id); + + // clearNotification(int id, NotificationType notifyType) will check if given type is equal + // to previous type or not. If they are equal then clear the notification; if they are not + // equal then return. + mManager.showNotification(id, NO_INTERNET, mWifiNai, mCellNai, null, false); + verify(mNotificationManager, times(1)).notify(eq(tag), eq(NO_INTERNET.eventId), any()); + + // Previous notification is NO_INTERNET and given type is NO_INTERNET too. The notification + // should be cleared. + mManager.clearNotification(id, NO_INTERNET); + verify(mNotificationManager, times(1)).cancel(eq(tag), eq(NO_INTERNET.eventId)); + + // SIGN_IN is popped-up. + mManager.showNotification(id, SIGN_IN, mWifiNai, mCellNai, null, false); + verify(mNotificationManager, times(1)).notify(eq(tag), eq(SIGN_IN.eventId), any()); + + // The notification type is not matching previous one, PARTIAL_CONNECTIVITY won't be + // cleared. + mManager.clearNotification(id, PARTIAL_CONNECTIVITY); + verify(mNotificationManager, never()).cancel(eq(tag), eq(PARTIAL_CONNECTIVITY.eventId)); + } +} diff --git a/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt b/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt new file mode 100644 index 0000000000..86c91165f6 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2020 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 android.net.NetworkRequest +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import kotlin.test.assertEquals +import kotlin.test.assertNull + +@RunWith(AndroidJUnit4::class) +@SmallTest +class NetworkRankerTest { + private val ranker = NetworkRanker() + + private fun makeNai(satisfy: Boolean, score: Int) = mock(NetworkAgentInfo::class.java).also { + doReturn(satisfy).`when`(it).satisfies(any()) + doReturn(score).`when`(it).currentScore + } + + @Test + fun testGetBestNetwork() { + val scores = listOf(20, 50, 90, 60, 23, 68) + val nais = scores.map { makeNai(true, it) } + val bestNetwork = nais[2] // The one with the top score + val someRequest = mock(NetworkRequest::class.java) + assertEquals(bestNetwork, ranker.getBestNetwork(someRequest, nais)) + } + + @Test + fun testIgnoreNonSatisfying() { + val nais = listOf(makeNai(true, 20), makeNai(true, 50), makeNai(false, 90), + makeNai(false, 60), makeNai(true, 23), makeNai(false, 68)) + val bestNetwork = nais[1] // Top score that's satisfying + val someRequest = mock(NetworkRequest::class.java) + assertEquals(bestNetwork, ranker.getBestNetwork(someRequest, nais)) + } + + @Test + fun testNoMatch() { + val nais = listOf(makeNai(false, 20), makeNai(false, 50), makeNai(false, 90)) + val someRequest = mock(NetworkRequest::class.java) + assertNull(ranker.getBestNetwork(someRequest, nais)) + } + + @Test + fun testEmpty() { + val someRequest = mock(NetworkRequest::class.java) + assertNull(ranker.getBestNetwork(someRequest, emptyList())) + } + + // Make sure the ranker is "stable" (as in stable sort), that is, it always returns the FIRST + // network satisfying the request if multiple of them have the same score. + @Test + fun testStable() { + val nais1 = listOf(makeNai(true, 30), makeNai(true, 30), makeNai(true, 30), + makeNai(true, 30), makeNai(true, 30), makeNai(true, 30)) + val someRequest = mock(NetworkRequest::class.java) + assertEquals(nais1[0], ranker.getBestNetwork(someRequest, nais1)) + + val nais2 = listOf(makeNai(true, 30), makeNai(true, 50), makeNai(true, 20), + makeNai(true, 50), makeNai(true, 50), makeNai(true, 40)) + assertEquals(nais2[1], ranker.getBestNetwork(someRequest, nais2)) + } +} diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java new file mode 100644 index 0000000000..02a58080fe --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java @@ -0,0 +1,803 @@ +/* + * 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 com.android.server.connectivity; + +import static android.Manifest.permission.CHANGE_NETWORK_STATE; +import static android.Manifest.permission.CHANGE_WIFI_STATE; +import static android.Manifest.permission.CONNECTIVITY_INTERNAL; +import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS; +import static android.Manifest.permission.INTERNET; +import static android.Manifest.permission.NETWORK_STACK; +import static android.Manifest.permission.UPDATE_DEVICE_STATS; +import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_OEM; +import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_PRODUCT; +import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_VENDOR; +import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED; +import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_REQUIRED; +import static android.content.pm.PackageManager.GET_PERMISSIONS; +import static android.content.pm.PackageManager.MATCH_ANY_USER; +import static android.os.Process.SYSTEM_UID; + +import static com.android.server.connectivity.PermissionMonitor.NETWORK; +import static com.android.server.connectivity.PermissionMonitor.SYSTEM; + +import static junit.framework.Assert.fail; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +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.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.net.INetd; +import android.net.UidRange; +import android.net.Uri; +import android.os.Build; +import android.os.SystemConfigManager; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.SparseIntArray; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.AdditionalAnswers; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class PermissionMonitorTest { + private static final UserHandle MOCK_USER1 = UserHandle.of(0); + private static final UserHandle MOCK_USER2 = UserHandle.of(1); + private static final int MOCK_UID1 = 10001; + private static final int MOCK_UID2 = 10086; + private static final int SYSTEM_UID1 = 1000; + private static final int SYSTEM_UID2 = 1008; + private static final int VPN_UID = 10002; + private static final String REAL_SYSTEM_PACKAGE_NAME = "android"; + private static final String MOCK_PACKAGE1 = "appName1"; + private static final String MOCK_PACKAGE2 = "appName2"; + private static final String SYSTEM_PACKAGE1 = "sysName1"; + private static final String SYSTEM_PACKAGE2 = "sysName2"; + private static final String PARTITION_SYSTEM = "system"; + private static final String PARTITION_OEM = "oem"; + private static final String PARTITION_PRODUCT = "product"; + private static final String PARTITION_VENDOR = "vendor"; + private static final int VERSION_P = Build.VERSION_CODES.P; + private static final int VERSION_Q = Build.VERSION_CODES.Q; + + @Mock private Context mContext; + @Mock private PackageManager mPackageManager; + @Mock private INetd mNetdService; + @Mock private UserManager mUserManager; + @Mock private PermissionMonitor.Dependencies mDeps; + @Mock private SystemConfigManager mSystemConfigManager; + + private PermissionMonitor mPermissionMonitor; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mContext.getSystemService(eq(Context.USER_SERVICE))).thenReturn(mUserManager); + when(mUserManager.getUserHandles(eq(true))).thenReturn( + Arrays.asList(new UserHandle[] { MOCK_USER1, MOCK_USER2 })); + when(mContext.getSystemServiceName(SystemConfigManager.class)) + .thenReturn(Context.SYSTEM_CONFIG_SERVICE); + when(mContext.getSystemService(Context.SYSTEM_CONFIG_SERVICE)) + .thenReturn(mSystemConfigManager); + when(mSystemConfigManager.getSystemPermissionUids(anyString())).thenReturn(new int[0]); + final Context asUserCtx = mock(Context.class, AdditionalAnswers.delegatesTo(mContext)); + doReturn(UserHandle.ALL).when(asUserCtx).getUser(); + when(mContext.createContextAsUser(eq(UserHandle.ALL), anyInt())).thenReturn(asUserCtx); + + mPermissionMonitor = spy(new PermissionMonitor(mContext, mNetdService, mDeps)); + + when(mPackageManager.getInstalledPackages(anyInt())).thenReturn(/* empty app list */ null); + mPermissionMonitor.startMonitoring(); + } + + private boolean hasRestrictedNetworkPermission(String partition, int targetSdkVersion, int uid, + String... permissions) { + final PackageInfo packageInfo = + packageInfoWithPermissions(REQUESTED_PERMISSION_GRANTED, permissions, partition); + packageInfo.applicationInfo.targetSdkVersion = targetSdkVersion; + packageInfo.applicationInfo.uid = uid; + return mPermissionMonitor.hasRestrictedNetworkPermission(packageInfo); + } + + private static PackageInfo systemPackageInfoWithPermissions(String... permissions) { + return packageInfoWithPermissions( + REQUESTED_PERMISSION_GRANTED, permissions, PARTITION_SYSTEM); + } + + private static PackageInfo vendorPackageInfoWithPermissions(String... permissions) { + return packageInfoWithPermissions( + REQUESTED_PERMISSION_GRANTED, permissions, PARTITION_VENDOR); + } + + private static PackageInfo packageInfoWithPermissions(int permissionsFlags, + String[] permissions, String partition) { + int[] requestedPermissionsFlags = new int[permissions.length]; + for (int i = 0; i < permissions.length; i++) { + requestedPermissionsFlags[i] = permissionsFlags; + } + final PackageInfo packageInfo = new PackageInfo(); + packageInfo.requestedPermissions = permissions; + packageInfo.applicationInfo = new ApplicationInfo(); + packageInfo.requestedPermissionsFlags = requestedPermissionsFlags; + int privateFlags = 0; + switch (partition) { + case PARTITION_OEM: + privateFlags = PRIVATE_FLAG_OEM; + break; + case PARTITION_PRODUCT: + privateFlags = PRIVATE_FLAG_PRODUCT; + break; + case PARTITION_VENDOR: + privateFlags = PRIVATE_FLAG_VENDOR; + break; + } + packageInfo.applicationInfo.privateFlags = privateFlags; + return packageInfo; + } + + private static PackageInfo buildPackageInfo(boolean hasSystemPermission, int uid, + UserHandle user) { + final PackageInfo pkgInfo; + if (hasSystemPermission) { + pkgInfo = systemPackageInfoWithPermissions( + CHANGE_NETWORK_STATE, NETWORK_STACK, CONNECTIVITY_USE_RESTRICTED_NETWORKS); + } else { + pkgInfo = packageInfoWithPermissions(REQUESTED_PERMISSION_GRANTED, new String[] {}, ""); + } + pkgInfo.applicationInfo.uid = user.getUid(UserHandle.getAppId(uid)); + return pkgInfo; + } + + @Test + public void testHasPermission() { + PackageInfo app = systemPackageInfoWithPermissions(); + assertFalse(mPermissionMonitor.hasPermission(app, CHANGE_NETWORK_STATE)); + assertFalse(mPermissionMonitor.hasPermission(app, NETWORK_STACK)); + assertFalse(mPermissionMonitor.hasPermission(app, CONNECTIVITY_USE_RESTRICTED_NETWORKS)); + assertFalse(mPermissionMonitor.hasPermission(app, CONNECTIVITY_INTERNAL)); + + app = systemPackageInfoWithPermissions(CHANGE_NETWORK_STATE, NETWORK_STACK); + assertTrue(mPermissionMonitor.hasPermission(app, CHANGE_NETWORK_STATE)); + assertTrue(mPermissionMonitor.hasPermission(app, NETWORK_STACK)); + assertFalse(mPermissionMonitor.hasPermission(app, CONNECTIVITY_USE_RESTRICTED_NETWORKS)); + assertFalse(mPermissionMonitor.hasPermission(app, CONNECTIVITY_INTERNAL)); + + app = systemPackageInfoWithPermissions( + CONNECTIVITY_USE_RESTRICTED_NETWORKS, CONNECTIVITY_INTERNAL); + assertFalse(mPermissionMonitor.hasPermission(app, CHANGE_NETWORK_STATE)); + assertFalse(mPermissionMonitor.hasPermission(app, NETWORK_STACK)); + assertTrue(mPermissionMonitor.hasPermission(app, CONNECTIVITY_USE_RESTRICTED_NETWORKS)); + assertTrue(mPermissionMonitor.hasPermission(app, CONNECTIVITY_INTERNAL)); + + app = packageInfoWithPermissions(REQUESTED_PERMISSION_REQUIRED, new String[] { + CONNECTIVITY_USE_RESTRICTED_NETWORKS, CONNECTIVITY_INTERNAL, NETWORK_STACK }, + PARTITION_SYSTEM); + assertFalse(mPermissionMonitor.hasPermission(app, CHANGE_NETWORK_STATE)); + assertFalse(mPermissionMonitor.hasPermission(app, NETWORK_STACK)); + assertFalse(mPermissionMonitor.hasPermission(app, CONNECTIVITY_USE_RESTRICTED_NETWORKS)); + assertFalse(mPermissionMonitor.hasPermission(app, CONNECTIVITY_INTERNAL)); + + app = systemPackageInfoWithPermissions(CHANGE_NETWORK_STATE); + app.requestedPermissions = null; + assertFalse(mPermissionMonitor.hasPermission(app, CHANGE_NETWORK_STATE)); + + app = systemPackageInfoWithPermissions(CHANGE_NETWORK_STATE); + app.requestedPermissionsFlags = null; + assertFalse(mPermissionMonitor.hasPermission(app, CHANGE_NETWORK_STATE)); + } + + @Test + public void testIsVendorApp() { + PackageInfo app = systemPackageInfoWithPermissions(); + assertFalse(mPermissionMonitor.isVendorApp(app.applicationInfo)); + app = packageInfoWithPermissions(REQUESTED_PERMISSION_GRANTED, + new String[] {}, PARTITION_OEM); + assertTrue(mPermissionMonitor.isVendorApp(app.applicationInfo)); + app = packageInfoWithPermissions(REQUESTED_PERMISSION_GRANTED, + new String[] {}, PARTITION_PRODUCT); + assertTrue(mPermissionMonitor.isVendorApp(app.applicationInfo)); + app = vendorPackageInfoWithPermissions(); + assertTrue(mPermissionMonitor.isVendorApp(app.applicationInfo)); + } + + @Test + public void testHasNetworkPermission() { + PackageInfo app = systemPackageInfoWithPermissions(); + assertFalse(mPermissionMonitor.hasNetworkPermission(app)); + app = systemPackageInfoWithPermissions(CHANGE_NETWORK_STATE); + assertTrue(mPermissionMonitor.hasNetworkPermission(app)); + app = systemPackageInfoWithPermissions(NETWORK_STACK); + assertFalse(mPermissionMonitor.hasNetworkPermission(app)); + app = systemPackageInfoWithPermissions(CONNECTIVITY_USE_RESTRICTED_NETWORKS); + assertFalse(mPermissionMonitor.hasNetworkPermission(app)); + app = systemPackageInfoWithPermissions(CONNECTIVITY_INTERNAL); + assertFalse(mPermissionMonitor.hasNetworkPermission(app)); + } + + @Test + public void testHasRestrictedNetworkPermission() { + assertFalse(hasRestrictedNetworkPermission(PARTITION_SYSTEM, VERSION_P, MOCK_UID1)); + assertFalse(hasRestrictedNetworkPermission( + PARTITION_SYSTEM, VERSION_P, MOCK_UID1, CHANGE_NETWORK_STATE)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_SYSTEM, VERSION_P, MOCK_UID1, NETWORK_STACK)); + assertFalse(hasRestrictedNetworkPermission( + PARTITION_SYSTEM, VERSION_P, MOCK_UID1, CONNECTIVITY_INTERNAL)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_SYSTEM, VERSION_P, MOCK_UID1, CONNECTIVITY_USE_RESTRICTED_NETWORKS)); + assertFalse(hasRestrictedNetworkPermission( + PARTITION_SYSTEM, VERSION_P, MOCK_UID1, CHANGE_WIFI_STATE)); + + assertFalse(hasRestrictedNetworkPermission(PARTITION_SYSTEM, VERSION_Q, MOCK_UID1)); + assertFalse(hasRestrictedNetworkPermission( + PARTITION_SYSTEM, VERSION_Q, MOCK_UID1, CONNECTIVITY_INTERNAL)); + } + + @Test + public void testHasRestrictedNetworkPermissionSystemUid() { + doReturn(VERSION_P).when(mDeps).getDeviceFirstSdkInt(); + assertTrue(hasRestrictedNetworkPermission(PARTITION_SYSTEM, VERSION_P, SYSTEM_UID)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_SYSTEM, VERSION_P, SYSTEM_UID, CONNECTIVITY_INTERNAL)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_SYSTEM, VERSION_P, SYSTEM_UID, CONNECTIVITY_USE_RESTRICTED_NETWORKS)); + + doReturn(VERSION_Q).when(mDeps).getDeviceFirstSdkInt(); + assertFalse(hasRestrictedNetworkPermission(PARTITION_SYSTEM, VERSION_Q, SYSTEM_UID)); + assertFalse(hasRestrictedNetworkPermission( + PARTITION_SYSTEM, VERSION_Q, SYSTEM_UID, CONNECTIVITY_INTERNAL)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_SYSTEM, VERSION_Q, SYSTEM_UID, CONNECTIVITY_USE_RESTRICTED_NETWORKS)); + } + + @Test + public void testHasRestrictedNetworkPermissionVendorApp() { + assertTrue(hasRestrictedNetworkPermission(PARTITION_VENDOR, VERSION_P, MOCK_UID1)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_VENDOR, VERSION_P, MOCK_UID1, CHANGE_NETWORK_STATE)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_VENDOR, VERSION_P, MOCK_UID1, NETWORK_STACK)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_VENDOR, VERSION_P, MOCK_UID1, CONNECTIVITY_INTERNAL)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_VENDOR, VERSION_P, MOCK_UID1, CONNECTIVITY_USE_RESTRICTED_NETWORKS)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_VENDOR, VERSION_P, MOCK_UID1, CHANGE_WIFI_STATE)); + + assertFalse(hasRestrictedNetworkPermission(PARTITION_VENDOR, VERSION_Q, MOCK_UID1)); + assertFalse(hasRestrictedNetworkPermission( + PARTITION_VENDOR, VERSION_Q, MOCK_UID1, CONNECTIVITY_INTERNAL)); + assertFalse(hasRestrictedNetworkPermission( + PARTITION_VENDOR, VERSION_Q, MOCK_UID1, CHANGE_NETWORK_STATE)); + } + + private void assertBackgroundPermission(boolean hasPermission, String name, int uid, + String... permissions) throws Exception { + when(mPackageManager.getPackageInfo(eq(name), anyInt())) + .thenReturn(packageInfoWithPermissions( + REQUESTED_PERMISSION_GRANTED, permissions, PARTITION_SYSTEM)); + mPermissionMonitor.onPackageAdded(name, uid); + assertEquals(hasPermission, mPermissionMonitor.hasUseBackgroundNetworksPermission(uid)); + } + + @Test + public void testHasUseBackgroundNetworksPermission() throws Exception { + assertFalse(mPermissionMonitor.hasUseBackgroundNetworksPermission(SYSTEM_UID)); + assertBackgroundPermission(false, SYSTEM_PACKAGE1, SYSTEM_UID); + assertBackgroundPermission(false, SYSTEM_PACKAGE1, SYSTEM_UID, CONNECTIVITY_INTERNAL); + assertBackgroundPermission(true, SYSTEM_PACKAGE1, SYSTEM_UID, CHANGE_NETWORK_STATE); + assertBackgroundPermission(true, SYSTEM_PACKAGE1, SYSTEM_UID, NETWORK_STACK); + + assertFalse(mPermissionMonitor.hasUseBackgroundNetworksPermission(MOCK_UID1)); + assertBackgroundPermission(false, MOCK_PACKAGE1, MOCK_UID1); + assertBackgroundPermission(true, MOCK_PACKAGE1, MOCK_UID1, + CONNECTIVITY_USE_RESTRICTED_NETWORKS); + + assertFalse(mPermissionMonitor.hasUseBackgroundNetworksPermission(MOCK_UID2)); + assertBackgroundPermission(false, MOCK_PACKAGE2, MOCK_UID2); + assertBackgroundPermission(false, MOCK_PACKAGE2, MOCK_UID2, + CONNECTIVITY_INTERNAL); + assertBackgroundPermission(true, MOCK_PACKAGE2, MOCK_UID2, NETWORK_STACK); + } + + private class NetdMonitor { + private final HashMap mApps = new HashMap<>(); + + NetdMonitor(INetd mockNetd) throws Exception { + // Add hook to verify and track result of setPermission. + doAnswer((InvocationOnMock invocation) -> { + final Object[] args = invocation.getArguments(); + final Boolean isSystem = args[0].equals(INetd.PERMISSION_SYSTEM); + for (final int uid : (int[]) args[1]) { + // TODO: Currently, permission monitor will send duplicate commands for each uid + // corresponding to each user. Need to fix that and uncomment below test. + // if (mApps.containsKey(uid) && mApps.get(uid) == isSystem) { + // fail("uid " + uid + " is already set to " + isSystem); + // } + mApps.put(uid, isSystem); + } + return null; + }).when(mockNetd).networkSetPermissionForUser(anyInt(), any(int[].class)); + + // Add hook to verify and track result of clearPermission. + doAnswer((InvocationOnMock invocation) -> { + final Object[] args = invocation.getArguments(); + for (final int uid : (int[]) args[0]) { + // TODO: Currently, permission monitor will send duplicate commands for each uid + // corresponding to each user. Need to fix that and uncomment below test. + // if (!mApps.containsKey(uid)) { + // fail("uid " + uid + " does not exist."); + // } + mApps.remove(uid); + } + return null; + }).when(mockNetd).networkClearPermissionForUser(any(int[].class)); + } + + public void expectPermission(Boolean permission, UserHandle[] users, int[] apps) { + for (final UserHandle user : users) { + for (final int app : apps) { + final int uid = user.getUid(app); + if (!mApps.containsKey(uid)) { + fail("uid " + uid + " does not exist."); + } + if (mApps.get(uid) != permission) { + fail("uid " + uid + " has wrong permission: " + permission); + } + } + } + } + + public void expectNoPermission(UserHandle[] users, int[] apps) { + for (final UserHandle user : users) { + for (final int app : apps) { + final int uid = user.getUid(app); + if (mApps.containsKey(uid)) { + fail("uid " + uid + " has listed permissions, expected none."); + } + } + } + } + } + + @Test + public void testUserAndPackageAddRemove() throws Exception { + final NetdMonitor mNetdMonitor = new NetdMonitor(mNetdService); + + // MOCK_UID1: MOCK_PACKAGE1 only has network permission. + // SYSTEM_UID: SYSTEM_PACKAGE1 has system permission. + // SYSTEM_UID: SYSTEM_PACKAGE2 only has network permission. + doReturn(SYSTEM).when(mPermissionMonitor).highestPermissionForUid(eq(SYSTEM), anyString()); + doReturn(SYSTEM).when(mPermissionMonitor).highestPermissionForUid(any(), + eq(SYSTEM_PACKAGE1)); + doReturn(NETWORK).when(mPermissionMonitor).highestPermissionForUid(any(), + eq(SYSTEM_PACKAGE2)); + doReturn(NETWORK).when(mPermissionMonitor).highestPermissionForUid(any(), + eq(MOCK_PACKAGE1)); + + // Add SYSTEM_PACKAGE2, expect only have network permission. + mPermissionMonitor.onUserAdded(MOCK_USER1); + addPackageForUsers(new UserHandle[]{MOCK_USER1}, SYSTEM_PACKAGE2, SYSTEM_UID); + mNetdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1}, new int[]{SYSTEM_UID}); + + // Add SYSTEM_PACKAGE1, expect permission escalate. + addPackageForUsers(new UserHandle[]{MOCK_USER1}, SYSTEM_PACKAGE1, SYSTEM_UID); + mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{SYSTEM_UID}); + + mPermissionMonitor.onUserAdded(MOCK_USER2); + mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1, MOCK_USER2}, + new int[]{SYSTEM_UID}); + + addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_UID1); + mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1, MOCK_USER2}, + new int[]{SYSTEM_UID}); + mNetdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1, MOCK_USER2}, + new int[]{MOCK_UID1}); + + // Remove MOCK_UID1, expect no permission left for all user. + mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID1); + removePackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_UID1); + mNetdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1, MOCK_USER2}, + new int[]{MOCK_UID1}); + + // Remove SYSTEM_PACKAGE1, expect permission downgrade. + when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(new String[]{SYSTEM_PACKAGE2}); + removePackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, + SYSTEM_PACKAGE1, SYSTEM_UID); + mNetdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1, MOCK_USER2}, + new int[]{SYSTEM_UID}); + + mPermissionMonitor.onUserRemoved(MOCK_USER1); + mNetdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER2}, new int[]{SYSTEM_UID}); + + // Remove all packages, expect no permission left. + when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(new String[]{}); + removePackageForUsers(new UserHandle[]{MOCK_USER2}, SYSTEM_PACKAGE2, SYSTEM_UID); + mNetdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1, MOCK_USER2}, + new int[]{SYSTEM_UID, MOCK_UID1}); + + // Remove last user, expect no redundant clearPermission is invoked. + mPermissionMonitor.onUserRemoved(MOCK_USER2); + mNetdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1, MOCK_USER2}, + new int[]{SYSTEM_UID, MOCK_UID1}); + } + + @Test + public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception { + when(mPackageManager.getInstalledPackages(eq(GET_PERMISSIONS | MATCH_ANY_USER))).thenReturn( + Arrays.asList(new PackageInfo[] { + buildPackageInfo(true /* hasSystemPermission */, SYSTEM_UID1, MOCK_USER1), + buildPackageInfo(false /* hasSystemPermission */, MOCK_UID1, MOCK_USER1), + buildPackageInfo(false /* hasSystemPermission */, MOCK_UID2, MOCK_USER1), + buildPackageInfo(false /* hasSystemPermission */, VPN_UID, MOCK_USER1) + })); + when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE1), + eq(GET_PERMISSIONS | MATCH_ANY_USER))).thenReturn( + buildPackageInfo(false /* hasSystemPermission */, MOCK_UID1, MOCK_USER1)); + mPermissionMonitor.startMonitoring(); + // Every app on user 0 except MOCK_UID2 are under VPN. + final Set vpnRange1 = new HashSet<>(Arrays.asList(new UidRange[] { + new UidRange(0, MOCK_UID2 - 1), + new UidRange(MOCK_UID2 + 1, UserHandle.PER_USER_RANGE - 1)})); + final Set vpnRange2 = Collections.singleton(new UidRange(MOCK_UID2, MOCK_UID2)); + + // When VPN is connected, expect a rule to be set up for user app MOCK_UID1 + mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange1, VPN_UID); + verify(mNetdService).firewallAddUidInterfaceRules(eq("tun0"), + aryEq(new int[] {MOCK_UID1})); + + reset(mNetdService); + + // When MOCK_UID1 package is uninstalled and reinstalled, expect Netd to be updated + mPermissionMonitor.onPackageRemoved( + MOCK_PACKAGE1, MOCK_USER1.getUid(MOCK_UID1)); + verify(mNetdService).firewallRemoveUidInterfaceRules(aryEq(new int[] {MOCK_UID1})); + mPermissionMonitor.onPackageAdded(MOCK_PACKAGE1, MOCK_USER1.getUid(MOCK_UID1)); + verify(mNetdService).firewallAddUidInterfaceRules(eq("tun0"), + aryEq(new int[] {MOCK_UID1})); + + reset(mNetdService); + + // During VPN uid update (vpnRange1 -> vpnRange2), ConnectivityService first deletes the + // old UID rules then adds the new ones. Expect netd to be updated + mPermissionMonitor.onVpnUidRangesRemoved("tun0", vpnRange1, VPN_UID); + verify(mNetdService).firewallRemoveUidInterfaceRules(aryEq(new int[] {MOCK_UID1})); + mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange2, VPN_UID); + verify(mNetdService).firewallAddUidInterfaceRules(eq("tun0"), + aryEq(new int[] {MOCK_UID2})); + + reset(mNetdService); + + // When VPN is disconnected, expect rules to be torn down + mPermissionMonitor.onVpnUidRangesRemoved("tun0", vpnRange2, VPN_UID); + verify(mNetdService).firewallRemoveUidInterfaceRules(aryEq(new int[] {MOCK_UID2})); + assertNull(mPermissionMonitor.getVpnUidRanges("tun0")); + } + + @Test + public void testUidFilteringDuringPackageInstallAndUninstall() throws Exception { + when(mPackageManager.getInstalledPackages(eq(GET_PERMISSIONS | MATCH_ANY_USER))).thenReturn( + Arrays.asList(new PackageInfo[] { + buildPackageInfo(true /* hasSystemPermission */, SYSTEM_UID1, MOCK_USER1), + buildPackageInfo(false /* hasSystemPermission */, VPN_UID, MOCK_USER1) + })); + when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE1), + eq(GET_PERMISSIONS | MATCH_ANY_USER))).thenReturn( + buildPackageInfo(false /* hasSystemPermission */, MOCK_UID1, MOCK_USER1)); + + mPermissionMonitor.startMonitoring(); + final Set vpnRange = Collections.singleton(UidRange.createForUser(MOCK_USER1)); + mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange, VPN_UID); + + // Newly-installed package should have uid rules added + mPermissionMonitor.onPackageAdded(MOCK_PACKAGE1, MOCK_USER1.getUid(MOCK_UID1)); + verify(mNetdService).firewallAddUidInterfaceRules(eq("tun0"), + aryEq(new int[] {MOCK_UID1})); + + // Removed package should have its uid rules removed + mPermissionMonitor.onPackageRemoved( + MOCK_PACKAGE1, MOCK_USER1.getUid(MOCK_UID1)); + verify(mNetdService).firewallRemoveUidInterfaceRules(aryEq(new int[] {MOCK_UID1})); + } + + + // Normal package add/remove operations will trigger multiple intent for uids corresponding to + // each user. To simulate generic package operations, the onPackageAdded/Removed will need to be + // called multiple times with the uid corresponding to each user. + private void addPackageForUsers(UserHandle[] users, String packageName, int uid) { + for (final UserHandle user : users) { + mPermissionMonitor.onPackageAdded(packageName, user.getUid(uid)); + } + } + + private void removePackageForUsers(UserHandle[] users, String packageName, int uid) { + for (final UserHandle user : users) { + mPermissionMonitor.onPackageRemoved(packageName, user.getUid(uid)); + } + } + + private class NetdServiceMonitor { + private final HashMap mPermissions = new HashMap<>(); + + NetdServiceMonitor(INetd mockNetdService) throws Exception { + // Add hook to verify and track result of setPermission. + doAnswer((InvocationOnMock invocation) -> { + final Object[] args = invocation.getArguments(); + final int permission = (int) args[0]; + for (final int uid : (int[]) args[1]) { + mPermissions.put(uid, permission); + } + return null; + }).when(mockNetdService).trafficSetNetPermForUids(anyInt(), any(int[].class)); + } + + public void expectPermission(int permission, int[] apps) { + for (final int app : apps) { + if (!mPermissions.containsKey(app)) { + fail("uid " + app + " does not exist."); + } + if (mPermissions.get(app) != permission) { + fail("uid " + app + " has wrong permission: " + mPermissions.get(app)); + } + } + } + } + + @Test + public void testPackagePermissionUpdate() throws Exception { + final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService); + // MOCK_UID1: MOCK_PACKAGE1 only has internet permission. + // MOCK_UID2: MOCK_PACKAGE2 does not have any permission. + // SYSTEM_UID1: SYSTEM_PACKAGE1 has internet permission and update device stats permission. + // SYSTEM_UID2: SYSTEM_PACKAGE2 has only update device stats permission. + + SparseIntArray netdPermissionsAppIds = new SparseIntArray(); + netdPermissionsAppIds.put(MOCK_UID1, INetd.PERMISSION_INTERNET); + netdPermissionsAppIds.put(MOCK_UID2, INetd.PERMISSION_NONE); + netdPermissionsAppIds.put(SYSTEM_UID1, INetd.PERMISSION_INTERNET + | INetd.PERMISSION_UPDATE_DEVICE_STATS); + netdPermissionsAppIds.put(SYSTEM_UID2, INetd.PERMISSION_UPDATE_DEVICE_STATS); + + // Send the permission information to netd, expect permission updated. + mPermissionMonitor.sendPackagePermissionsToNetd(netdPermissionsAppIds); + + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, + new int[]{MOCK_UID1}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_NONE, new int[]{MOCK_UID2}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET + | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{SYSTEM_UID1}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_UPDATE_DEVICE_STATS, + new int[]{SYSTEM_UID2}); + + // Update permission of MOCK_UID1, expect new permission show up. + mPermissionMonitor.sendPackagePermissionsForUid(MOCK_UID1, + INetd.PERMISSION_INTERNET | INetd.PERMISSION_UPDATE_DEVICE_STATS); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET + | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1}); + + // Change permissions of SYSTEM_UID2, expect new permission show up and old permission + // revoked. + mPermissionMonitor.sendPackagePermissionsForUid(SYSTEM_UID2, + INetd.PERMISSION_INTERNET); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{SYSTEM_UID2}); + + // Revoke permission from SYSTEM_UID1, expect no permission stored. + mPermissionMonitor.sendPackagePermissionsForUid(SYSTEM_UID1, INetd.PERMISSION_NONE); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_NONE, new int[]{SYSTEM_UID1}); + } + + private PackageInfo setPackagePermissions(String packageName, int uid, String[] permissions) + throws Exception { + PackageInfo packageInfo = packageInfoWithPermissions( + REQUESTED_PERMISSION_GRANTED, permissions, PARTITION_SYSTEM); + when(mPackageManager.getPackageInfo(eq(packageName), anyInt())).thenReturn(packageInfo); + when(mPackageManager.getPackagesForUid(eq(uid))).thenReturn(new String[]{packageName}); + return packageInfo; + } + + private PackageInfo addPackage(String packageName, int uid, String[] permissions) + throws Exception { + PackageInfo packageInfo = setPackagePermissions(packageName, uid, permissions); + mPermissionMonitor.onPackageAdded(packageName, uid); + return packageInfo; + } + + @Test + public void testPackageInstall() throws Exception { + final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService); + + addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET, UPDATE_DEVICE_STATS}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET + | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1}); + + addPackage(MOCK_PACKAGE2, MOCK_UID2, new String[] {INTERNET}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{MOCK_UID2}); + } + + @Test + public void testPackageInstallSharedUid() throws Exception { + final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService); + + PackageInfo packageInfo1 = addPackage(MOCK_PACKAGE1, MOCK_UID1, + new String[] {INTERNET, UPDATE_DEVICE_STATS}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET + | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1}); + + // Install another package with the same uid and no permissions should not cause the UID to + // lose permissions. + PackageInfo packageInfo2 = systemPackageInfoWithPermissions(); + when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE2), anyInt())).thenReturn(packageInfo2); + when(mPackageManager.getPackagesForUid(MOCK_UID1)) + .thenReturn(new String[]{MOCK_PACKAGE1, MOCK_PACKAGE2}); + mPermissionMonitor.onPackageAdded(MOCK_PACKAGE2, MOCK_UID1); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET + | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1}); + } + + @Test + public void testPackageUninstallBasic() throws Exception { + final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService); + + addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET, UPDATE_DEVICE_STATS}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET + | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1}); + + when(mPackageManager.getPackagesForUid(MOCK_UID1)).thenReturn(new String[]{}); + mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID1); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_UNINSTALLED, new int[]{MOCK_UID1}); + } + + @Test + public void testPackageRemoveThenAdd() throws Exception { + final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService); + + addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET, UPDATE_DEVICE_STATS}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET + | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1}); + + when(mPackageManager.getPackagesForUid(MOCK_UID1)).thenReturn(new String[]{}); + mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID1); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_UNINSTALLED, new int[]{MOCK_UID1}); + + addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{MOCK_UID1}); + } + + @Test + public void testPackageUpdate() throws Exception { + final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService); + + addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_NONE, new int[]{MOCK_UID1}); + + addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{MOCK_UID1}); + } + + @Test + public void testPackageUninstallWithMultiplePackages() throws Exception { + final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService); + + addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET, UPDATE_DEVICE_STATS}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET + | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1}); + + // Mock another package with the same uid but different permissions. + PackageInfo packageInfo2 = systemPackageInfoWithPermissions(INTERNET); + when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE2), anyInt())).thenReturn(packageInfo2); + when(mPackageManager.getPackagesForUid(MOCK_UID1)).thenReturn(new String[]{ + MOCK_PACKAGE2}); + + mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID1); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{MOCK_UID1}); + } + + @Test + public void testRealSystemPermission() throws Exception { + // Use the real context as this test must ensure the *real* system package holds the + // necessary permission. + final Context realContext = InstrumentationRegistry.getContext(); + final PermissionMonitor monitor = new PermissionMonitor(realContext, mNetdService); + final PackageManager manager = realContext.getPackageManager(); + final PackageInfo systemInfo = manager.getPackageInfo(REAL_SYSTEM_PACKAGE_NAME, + GET_PERMISSIONS | MATCH_ANY_USER); + assertTrue(monitor.hasPermission(systemInfo, CONNECTIVITY_USE_RESTRICTED_NETWORKS)); + } + + @Test + public void testUpdateUidPermissionsFromSystemConfig() throws Exception { + final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService); + when(mPackageManager.getInstalledPackages(anyInt())).thenReturn(new ArrayList<>()); + when(mSystemConfigManager.getSystemPermissionUids(eq(INTERNET))) + .thenReturn(new int[]{ MOCK_UID1, MOCK_UID2 }); + when(mSystemConfigManager.getSystemPermissionUids(eq(UPDATE_DEVICE_STATS))) + .thenReturn(new int[]{ MOCK_UID2 }); + + mPermissionMonitor.startMonitoring(); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{ MOCK_UID1 }); + mNetdServiceMonitor.expectPermission( + INetd.PERMISSION_INTERNET | INetd.PERMISSION_UPDATE_DEVICE_STATS, + new int[]{ MOCK_UID2 }); + } + + @Test + public void testIntentReceiver() throws Exception { + final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService); + final ArgumentCaptor receiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + verify(mContext, times(1)).registerReceiver(receiverCaptor.capture(), any(), any(), any()); + final BroadcastReceiver receiver = receiverCaptor.getValue(); + + // Verify receiving PACKAGE_ADDED intent. + final Intent addedIntent = new Intent(Intent.ACTION_PACKAGE_ADDED, + Uri.fromParts("package", MOCK_PACKAGE1, null /* fragment */)); + addedIntent.putExtra(Intent.EXTRA_UID, MOCK_UID1); + setPackagePermissions(MOCK_PACKAGE1, MOCK_UID1, + new String[] { INTERNET, UPDATE_DEVICE_STATS }); + receiver.onReceive(mContext, addedIntent); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET + | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[] { MOCK_UID1 }); + + // Verify receiving PACKAGE_REMOVED intent. + when(mPackageManager.getPackagesForUid(MOCK_UID1)).thenReturn(null); + final Intent removedIntent = new Intent(Intent.ACTION_PACKAGE_REMOVED, + Uri.fromParts("package", MOCK_PACKAGE1, null /* fragment */)); + removedIntent.putExtra(Intent.EXTRA_UID, MOCK_UID1); + receiver.onReceive(mContext, removedIntent); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_UNINSTALLED, new int[] { MOCK_UID1 }); + } + +} diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java new file mode 100644 index 0000000000..b725b826b1 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java @@ -0,0 +1,1283 @@ +/* + * Copyright (C) 2016 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.content.pm.UserInfo.FLAG_ADMIN; +import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE; +import static android.content.pm.UserInfo.FLAG_PRIMARY; +import static android.content.pm.UserInfo.FLAG_RESTRICTED; +import static android.net.ConnectivityManager.NetworkCallback; +import static android.net.INetd.IF_STATE_DOWN; +import static android.net.INetd.IF_STATE_UP; +import static android.os.UserHandle.PER_USER_RANGE; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +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.annotation.NonNull; +import android.annotation.UserIdInt; +import android.app.AppOpsManager; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.pm.UserInfo; +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.net.INetd; +import android.net.Ikev2VpnProfile; +import android.net.InetAddresses; +import android.net.InterfaceConfigurationParcel; +import android.net.IpPrefix; +import android.net.IpSecManager; +import android.net.IpSecTunnelInterfaceResponse; +import android.net.LinkProperties; +import android.net.LocalSocket; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo.DetailedState; +import android.net.RouteInfo; +import android.net.UidRangeParcel; +import android.net.VpnManager; +import android.net.VpnService; +import android.net.VpnTransportInfo; +import android.net.ipsec.ike.IkeSessionCallback; +import android.net.ipsec.ike.exceptions.IkeProtocolException; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.os.ConditionVariable; +import android.os.INetworkManagementService; +import android.os.Process; +import android.os.UserHandle; +import android.os.UserManager; +import android.os.test.TestLooper; +import android.provider.Settings; +import android.security.Credentials; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Range; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.R; +import com.android.internal.net.LegacyVpnInfo; +import com.android.internal.net.VpnConfig; +import com.android.internal.net.VpnProfile; +import com.android.server.IpSecService; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.AdditionalAnswers; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +/** + * Tests for {@link Vpn}. + * + * Build, install and run with: + * runtest frameworks-net -c com.android.server.connectivity.VpnTest + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class VpnTest { + private static final String TAG = "VpnTest"; + + // Mock users + static final UserInfo primaryUser = new UserInfo(27, "Primary", FLAG_ADMIN | FLAG_PRIMARY); + static final UserInfo secondaryUser = new UserInfo(15, "Secondary", FLAG_ADMIN); + static final UserInfo restrictedProfileA = new UserInfo(40, "RestrictedA", FLAG_RESTRICTED); + static final UserInfo restrictedProfileB = new UserInfo(42, "RestrictedB", FLAG_RESTRICTED); + static final UserInfo managedProfileA = new UserInfo(45, "ManagedA", FLAG_MANAGED_PROFILE); + static { + restrictedProfileA.restrictedProfileParentId = primaryUser.id; + restrictedProfileB.restrictedProfileParentId = secondaryUser.id; + managedProfileA.profileGroupId = primaryUser.id; + } + + static final Network EGRESS_NETWORK = new Network(101); + static final String EGRESS_IFACE = "wlan0"; + static final String TEST_VPN_PKG = "com.testvpn.vpn"; + private static final String TEST_VPN_SERVER = "1.2.3.4"; + private static final String TEST_VPN_IDENTITY = "identity"; + private static final byte[] TEST_VPN_PSK = "psk".getBytes(); + + private static final Network TEST_NETWORK = new Network(Integer.MAX_VALUE); + private static final String TEST_IFACE_NAME = "TEST_IFACE"; + private static final int TEST_TUNNEL_RESOURCE_ID = 0x2345; + private static final long TEST_TIMEOUT_MS = 500L; + + /** + * Names and UIDs for some fake packages. Important points: + * - UID is ordered increasing. + * - One pair of packages have consecutive UIDs. + */ + static final String[] PKGS = {"com.example", "org.example", "net.example", "web.vpn"}; + static final int[] PKG_UIDS = {66, 77, 78, 400}; + + // Mock packages + static final Map mPackages = new ArrayMap<>(); + static { + for (int i = 0; i < PKGS.length; i++) { + mPackages.put(PKGS[i], PKG_UIDS[i]); + } + } + private static final Range PRI_USER_RANGE = uidRangeForUser(primaryUser.id); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext; + @Mock private UserManager mUserManager; + @Mock private PackageManager mPackageManager; + @Mock private INetworkManagementService mNetService; + @Mock private INetd mNetd; + @Mock private AppOpsManager mAppOps; + @Mock private NotificationManager mNotificationManager; + @Mock private Vpn.SystemServices mSystemServices; + @Mock private Vpn.Ikev2SessionCreator mIkev2SessionCreator; + @Mock private ConnectivityManager mConnectivityManager; + @Mock private IpSecService mIpSecService; + @Mock private VpnProfileStore mVpnProfileStore; + private final VpnProfile mVpnProfile; + + private IpSecManager mIpSecManager; + + public VpnTest() throws Exception { + // Build an actual VPN profile that is capable of being converted to and from an + // Ikev2VpnProfile + final Ikev2VpnProfile.Builder builder = + new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY); + builder.setAuthPsk(TEST_VPN_PSK); + mVpnProfile = builder.build().toVpnProfile(); + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mIpSecManager = new IpSecManager(mContext, mIpSecService); + + when(mContext.getPackageManager()).thenReturn(mPackageManager); + setMockedPackages(mPackages); + + when(mContext.getPackageName()).thenReturn(TEST_VPN_PKG); + when(mContext.getOpPackageName()).thenReturn(TEST_VPN_PKG); + when(mContext.getSystemServiceName(UserManager.class)) + .thenReturn(Context.USER_SERVICE); + when(mContext.getSystemService(eq(Context.USER_SERVICE))).thenReturn(mUserManager); + when(mContext.getSystemService(eq(Context.APP_OPS_SERVICE))).thenReturn(mAppOps); + when(mContext.getSystemServiceName(NotificationManager.class)) + .thenReturn(Context.NOTIFICATION_SERVICE); + when(mContext.getSystemService(eq(Context.NOTIFICATION_SERVICE))) + .thenReturn(mNotificationManager); + when(mContext.getSystemService(eq(Context.CONNECTIVITY_SERVICE))) + .thenReturn(mConnectivityManager); + when(mContext.getSystemServiceName(eq(ConnectivityManager.class))) + .thenReturn(Context.CONNECTIVITY_SERVICE); + when(mContext.getSystemService(eq(Context.IPSEC_SERVICE))).thenReturn(mIpSecManager); + when(mContext.getString(R.string.config_customVpnAlwaysOnDisconnectedDialogComponent)) + .thenReturn(Resources.getSystem().getString( + R.string.config_customVpnAlwaysOnDisconnectedDialogComponent)); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS)) + .thenReturn(true); + + // Used by {@link Notification.Builder} + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.targetSdkVersion = VERSION_CODES.CUR_DEVELOPMENT; + when(mContext.getApplicationInfo()).thenReturn(applicationInfo); + when(mPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())) + .thenReturn(applicationInfo); + + doNothing().when(mNetService).registerObserver(any()); + + // Deny all appops by default. + when(mAppOps.noteOpNoThrow(anyString(), anyInt(), anyString(), any(), any())) + .thenReturn(AppOpsManager.MODE_IGNORED); + + // Setup IpSecService + final IpSecTunnelInterfaceResponse tunnelResp = + new IpSecTunnelInterfaceResponse( + IpSecManager.Status.OK, TEST_TUNNEL_RESOURCE_ID, TEST_IFACE_NAME); + when(mIpSecService.createTunnelInterface(any(), any(), any(), any(), any())) + .thenReturn(tunnelResp); + } + + private Set> rangeSet(Range ... ranges) { + final Set> range = new ArraySet<>(); + for (Range r : ranges) range.add(r); + + return range; + } + + private static Range uidRangeForUser(int userId) { + return new Range(userId * PER_USER_RANGE, (userId + 1) * PER_USER_RANGE - 1); + } + + private Range uidRange(int start, int stop) { + return new Range(start, stop); + } + + @Test + public void testRestrictedProfilesAreAddedToVpn() { + setMockedUsers(primaryUser, secondaryUser, restrictedProfileA, restrictedProfileB); + + final Vpn vpn = createVpn(primaryUser.id); + + // Assume the user can have restricted profiles. + doReturn(true).when(mUserManager).canHaveRestrictedProfile(); + final Set> ranges = + vpn.createUserAndRestrictedProfilesRanges(primaryUser.id, null, null); + + assertEquals(rangeSet(PRI_USER_RANGE, uidRangeForUser(restrictedProfileA.id)), ranges); + } + + @Test + public void testManagedProfilesAreNotAddedToVpn() { + setMockedUsers(primaryUser, managedProfileA); + + final Vpn vpn = createVpn(primaryUser.id); + final Set> ranges = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id, + null, null); + + assertEquals(rangeSet(PRI_USER_RANGE), ranges); + } + + @Test + public void testAddUserToVpnOnlyAddsOneUser() { + setMockedUsers(primaryUser, restrictedProfileA, managedProfileA); + + final Vpn vpn = createVpn(primaryUser.id); + final Set> ranges = new ArraySet<>(); + vpn.addUserToRanges(ranges, primaryUser.id, null, null); + + assertEquals(rangeSet(PRI_USER_RANGE), ranges); + } + + @Test + public void testUidAllowAndDenylist() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + final Range user = PRI_USER_RANGE; + final int userStart = user.getLower(); + final int userStop = user.getUpper(); + final String[] packages = {PKGS[0], PKGS[1], PKGS[2]}; + + // Allowed list + final Set> allow = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id, + Arrays.asList(packages), null /* disallowedApplications */); + assertEquals(rangeSet( + uidRange(userStart + PKG_UIDS[0], userStart + PKG_UIDS[0]), + uidRange(userStart + PKG_UIDS[1], userStart + PKG_UIDS[2])), + allow); + + // Denied list + final Set> disallow = + vpn.createUserAndRestrictedProfilesRanges(primaryUser.id, + null /* allowedApplications */, Arrays.asList(packages)); + assertEquals(rangeSet( + uidRange(userStart, userStart + PKG_UIDS[0] - 1), + uidRange(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1), + /* Empty range between UIDS[1] and UIDS[2], should be excluded, */ + uidRange(userStart + PKG_UIDS[2] + 1, userStop)), + disallow); + } + + @Test + public void testGetAlwaysAndOnGetLockDown() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + + // Default state. + assertFalse(vpn.getAlwaysOn()); + assertFalse(vpn.getLockdown()); + + // Set always-on without lockdown. + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, Collections.emptyList())); + assertTrue(vpn.getAlwaysOn()); + assertFalse(vpn.getLockdown()); + + // Set always-on with lockdown. + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, Collections.emptyList())); + assertTrue(vpn.getAlwaysOn()); + assertTrue(vpn.getLockdown()); + + // Remove always-on configuration. + assertTrue(vpn.setAlwaysOnPackage(null, false, Collections.emptyList())); + assertFalse(vpn.getAlwaysOn()); + assertFalse(vpn.getLockdown()); + } + + @Test + public void testLockdownChangingPackage() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + final Range user = PRI_USER_RANGE; + final int userStart = user.getLower(); + final int userStop = user.getUpper(); + // Set always-on without lockdown. + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, null)); + + // Set always-on with lockdown. + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, null)); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1), + new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop) + })); + + // Switch to another app. + assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null)); + verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1), + new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop) + })); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart, userStart + PKG_UIDS[3] - 1), + new UidRangeParcel(userStart + PKG_UIDS[3] + 1, userStop) + })); + } + + @Test + public void testLockdownAllowlist() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + final Range user = PRI_USER_RANGE; + final int userStart = user.getLower(); + final int userStop = user.getUpper(); + // Set always-on with lockdown and allow app PKGS[2] from lockdown. + assertTrue(vpn.setAlwaysOnPackage( + PKGS[1], true, Collections.singletonList(PKGS[2]))); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1), + new UidRangeParcel(userStart + PKG_UIDS[2] + 1, userStop) + })); + // Change allowed app list to PKGS[3]. + assertTrue(vpn.setAlwaysOnPackage( + PKGS[1], true, Collections.singletonList(PKGS[3]))); + verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[2] + 1, userStop) + })); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1), + new UidRangeParcel(userStart + PKG_UIDS[3] + 1, userStop) + })); + + // Change the VPN app. + assertTrue(vpn.setAlwaysOnPackage( + PKGS[0], true, Collections.singletonList(PKGS[3]))); + verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1), + new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1) + })); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart, userStart + PKG_UIDS[0] - 1), + new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1) + })); + + // Remove the list of allowed packages. + assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, null)); + verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1), + new UidRangeParcel(userStart + PKG_UIDS[3] + 1, userStop) + })); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStop), + })); + + // Add the list of allowed packages. + assertTrue(vpn.setAlwaysOnPackage( + PKGS[0], true, Collections.singletonList(PKGS[1]))); + verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStop) + })); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1), + new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop) + })); + + // Try allowing a package with a comma, should be rejected. + assertFalse(vpn.setAlwaysOnPackage( + PKGS[0], true, Collections.singletonList("a.b,c.d"))); + + // Pass a non-existent packages in the allowlist, they (and only they) should be ignored. + // allowed package should change from PGKS[1] to PKGS[2]. + assertTrue(vpn.setAlwaysOnPackage( + PKGS[0], true, Arrays.asList("com.foo.app", PKGS[2], "com.bar.app"))); + verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1), + new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop) + })); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[2] - 1), + new UidRangeParcel(userStart + PKG_UIDS[2] + 1, userStop) + })); + } + + @Test + public void testLockdownRuleRepeatability() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] { + new UidRangeParcel(PRI_USER_RANGE.getLower(), PRI_USER_RANGE.getUpper())}; + // Given legacy lockdown is already enabled, + vpn.setLockdown(true); + verify(mConnectivityManager, times(1)).setRequireVpnForUids(true, + toRanges(primaryUserRangeParcel)); + + // Enabling legacy lockdown twice should do nothing. + vpn.setLockdown(true); + verify(mConnectivityManager, times(1)).setRequireVpnForUids(anyBoolean(), any()); + + // And disabling should remove the rules exactly once. + vpn.setLockdown(false); + verify(mConnectivityManager, times(1)).setRequireVpnForUids(false, + toRanges(primaryUserRangeParcel)); + + // Removing the lockdown again should have no effect. + vpn.setLockdown(false); + verify(mConnectivityManager, times(2)).setRequireVpnForUids(anyBoolean(), any()); + } + + private ArrayList> toRanges(UidRangeParcel[] ranges) { + ArrayList> rangesArray = new ArrayList<>(ranges.length); + for (int i = 0; i < ranges.length; i++) { + rangesArray.add(new Range<>(ranges[i].start, ranges[i].stop)); + } + return rangesArray; + } + + @Test + public void testLockdownRuleReversibility() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + final UidRangeParcel[] entireUser = { + new UidRangeParcel(PRI_USER_RANGE.getLower(), PRI_USER_RANGE.getUpper()) + }; + final UidRangeParcel[] exceptPkg0 = { + new UidRangeParcel(entireUser[0].start, entireUser[0].start + PKG_UIDS[0] - 1), + new UidRangeParcel(entireUser[0].start + PKG_UIDS[0] + 1, entireUser[0].stop) + }; + + final InOrder order = inOrder(mConnectivityManager); + + // Given lockdown is enabled with no package (legacy VPN), + vpn.setLockdown(true); + order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(entireUser)); + + // When a new VPN package is set the rules should change to cover that package. + vpn.prepare(null, PKGS[0], VpnManager.TYPE_VPN_SERVICE); + order.verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(entireUser)); + order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(exceptPkg0)); + + // When that VPN package is unset, everything should be undone again in reverse. + vpn.prepare(null, VpnConfig.LEGACY_VPN, VpnManager.TYPE_VPN_SERVICE); + order.verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(exceptPkg0)); + order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(entireUser)); + } + + @Test + public void testIsAlwaysOnPackageSupported() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + + ApplicationInfo appInfo = new ApplicationInfo(); + when(mPackageManager.getApplicationInfoAsUser(eq(PKGS[0]), anyInt(), eq(primaryUser.id))) + .thenReturn(appInfo); + + ServiceInfo svcInfo = new ServiceInfo(); + ResolveInfo resInfo = new ResolveInfo(); + resInfo.serviceInfo = svcInfo; + when(mPackageManager.queryIntentServicesAsUser(any(), eq(PackageManager.GET_META_DATA), + eq(primaryUser.id))) + .thenReturn(Collections.singletonList(resInfo)); + + // null package name should return false + assertFalse(vpn.isAlwaysOnPackageSupported(null)); + + // Pre-N apps are not supported + appInfo.targetSdkVersion = VERSION_CODES.M; + assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0])); + + // N+ apps are supported by default + appInfo.targetSdkVersion = VERSION_CODES.N; + assertTrue(vpn.isAlwaysOnPackageSupported(PKGS[0])); + + // Apps that opt out explicitly are not supported + appInfo.targetSdkVersion = VERSION_CODES.CUR_DEVELOPMENT; + Bundle metaData = new Bundle(); + metaData.putBoolean(VpnService.SERVICE_META_DATA_SUPPORTS_ALWAYS_ON, false); + svcInfo.metaData = metaData; + assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0])); + } + + @Test + public void testNotificationShownForAlwaysOnApp() throws Exception { + final UserHandle userHandle = UserHandle.of(primaryUser.id); + final Vpn vpn = createVpn(primaryUser.id); + setMockedUsers(primaryUser); + + final InOrder order = inOrder(mNotificationManager); + + // Don't show a notification for regular disconnected states. + vpn.updateState(DetailedState.DISCONNECTED, TAG); + order.verify(mNotificationManager, atLeastOnce()).cancel(anyString(), anyInt()); + + // Start showing a notification for disconnected once always-on. + vpn.setAlwaysOnPackage(PKGS[0], false, null); + order.verify(mNotificationManager).notify(anyString(), anyInt(), any()); + + // Stop showing the notification once connected. + vpn.updateState(DetailedState.CONNECTED, TAG); + order.verify(mNotificationManager).cancel(anyString(), anyInt()); + + // Show the notification if we disconnect again. + vpn.updateState(DetailedState.DISCONNECTED, TAG); + order.verify(mNotificationManager).notify(anyString(), anyInt(), any()); + + // Notification should be cleared after unsetting always-on package. + vpn.setAlwaysOnPackage(null, false, null); + order.verify(mNotificationManager).cancel(anyString(), anyInt()); + } + + /** + * The profile name should NOT change between releases for backwards compatibility + * + *

If this is changed between releases, the {@link Vpn#getVpnProfilePrivileged()} method MUST + * be updated to ensure backward compatibility. + */ + @Test + public void testGetProfileNameForPackage() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + setMockedUsers(primaryUser); + + final String expected = Credentials.PLATFORM_VPN + primaryUser.id + "_" + TEST_VPN_PKG; + assertEquals(expected, vpn.getProfileNameForPackage(TEST_VPN_PKG)); + } + + private Vpn createVpnAndSetupUidChecks(String... grantedOps) throws Exception { + return createVpnAndSetupUidChecks(primaryUser, grantedOps); + } + + private Vpn createVpnAndSetupUidChecks(UserInfo user, String... grantedOps) throws Exception { + final Vpn vpn = createVpn(user.id); + setMockedUsers(user); + + when(mPackageManager.getPackageUidAsUser(eq(TEST_VPN_PKG), anyInt())) + .thenReturn(Process.myUid()); + + for (final String opStr : grantedOps) { + when(mAppOps.noteOpNoThrow(opStr, Process.myUid(), TEST_VPN_PKG, + null /* attributionTag */, null /* message */)) + .thenReturn(AppOpsManager.MODE_ALLOWED); + } + + return vpn; + } + + private void checkProvisionVpnProfile(Vpn vpn, boolean expectedResult, String... checkedOps) { + assertEquals(expectedResult, vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile)); + + // The profile should always be stored, whether or not consent has been previously granted. + verify(mVpnProfileStore) + .put( + eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)), + eq(mVpnProfile.encode())); + + for (final String checkedOpStr : checkedOps) { + verify(mAppOps).noteOpNoThrow(checkedOpStr, Process.myUid(), TEST_VPN_PKG, + null /* attributionTag */, null /* message */); + } + } + + @Test + public void testProvisionVpnProfileNoIpsecTunnels() throws Exception { + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS)) + .thenReturn(false); + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + try { + checkProvisionVpnProfile( + vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + fail("Expected exception due to missing feature"); + } catch (UnsupportedOperationException expected) { + } + } + + @Test + public void testProvisionVpnProfilePreconsented() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + checkProvisionVpnProfile( + vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + } + + @Test + public void testProvisionVpnProfileNotPreconsented() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(); + + // Expect that both the ACTIVATE_VPN and ACTIVATE_PLATFORM_VPN were tried, but the caller + // had neither. + checkProvisionVpnProfile(vpn, false /* expectedResult */, + AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN, AppOpsManager.OPSTR_ACTIVATE_VPN); + } + + @Test + public void testProvisionVpnProfileVpnServicePreconsented() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_VPN); + + checkProvisionVpnProfile(vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_VPN); + } + + @Test + public void testProvisionVpnProfileTooLarge() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + final VpnProfile bigProfile = new VpnProfile(""); + bigProfile.name = new String(new byte[Vpn.MAX_VPN_PROFILE_SIZE_BYTES + 1]); + + try { + vpn.provisionVpnProfile(TEST_VPN_PKG, bigProfile); + fail("Expected IAE due to profile size"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testProvisionVpnProfileRestrictedUser() throws Exception { + final Vpn vpn = + createVpnAndSetupUidChecks( + restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + try { + vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile); + fail("Expected SecurityException due to restricted user"); + } catch (SecurityException expected) { + } + } + + @Test + public void testDeleteVpnProfile() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(); + + vpn.deleteVpnProfile(TEST_VPN_PKG); + + verify(mVpnProfileStore) + .remove(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); + } + + @Test + public void testDeleteVpnProfileRestrictedUser() throws Exception { + final Vpn vpn = + createVpnAndSetupUidChecks( + restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + try { + vpn.deleteVpnProfile(TEST_VPN_PKG); + fail("Expected SecurityException due to restricted user"); + } catch (SecurityException expected) { + } + } + + @Test + public void testGetVpnProfilePrivileged() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(); + + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(new VpnProfile("").encode()); + + vpn.getVpnProfilePrivileged(TEST_VPN_PKG); + + verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); + } + + @Test + public void testStartVpnProfile() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(mVpnProfile.encode()); + + vpn.startVpnProfile(TEST_VPN_PKG); + + verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); + verify(mAppOps) + .noteOpNoThrow( + eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(null) /* attributionTag */, + eq(null) /* message */); + } + + @Test + public void testStartVpnProfileVpnServicePreconsented() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_VPN); + + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(mVpnProfile.encode()); + + vpn.startVpnProfile(TEST_VPN_PKG); + + // Verify that the the ACTIVATE_VPN appop was checked, but no error was thrown. + verify(mAppOps).noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_VPN, Process.myUid(), + TEST_VPN_PKG, null /* attributionTag */, null /* message */); + } + + @Test + public void testStartVpnProfileNotConsented() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(); + + try { + vpn.startVpnProfile(TEST_VPN_PKG); + fail("Expected failure due to no user consent"); + } catch (SecurityException expected) { + } + + // Verify both appops were checked. + verify(mAppOps) + .noteOpNoThrow( + eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(null) /* attributionTag */, + eq(null) /* message */); + verify(mAppOps).noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_VPN, Process.myUid(), + TEST_VPN_PKG, null /* attributionTag */, null /* message */); + + // Keystore should never have been accessed. + verify(mVpnProfileStore, never()).get(any()); + } + + @Test + public void testStartVpnProfileMissingProfile() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))).thenReturn(null); + + try { + vpn.startVpnProfile(TEST_VPN_PKG); + fail("Expected failure due to missing profile"); + } catch (IllegalArgumentException expected) { + } + + verify(mVpnProfileStore).get(vpn.getProfileNameForPackage(TEST_VPN_PKG)); + verify(mAppOps) + .noteOpNoThrow( + eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(null) /* attributionTag */, + eq(null) /* message */); + } + + @Test + public void testStartVpnProfileRestrictedUser() throws Exception { + final Vpn vpn = + createVpnAndSetupUidChecks( + restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + try { + vpn.startVpnProfile(TEST_VPN_PKG); + fail("Expected SecurityException due to restricted user"); + } catch (SecurityException expected) { + } + } + + @Test + public void testStopVpnProfileRestrictedUser() throws Exception { + final Vpn vpn = + createVpnAndSetupUidChecks( + restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + try { + vpn.stopVpnProfile(TEST_VPN_PKG); + fail("Expected SecurityException due to restricted user"); + } catch (SecurityException expected) { + } + } + + @Test + public void testSetPackageAuthorizationVpnService() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(); + + assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_SERVICE)); + verify(mAppOps) + .setMode( + eq(AppOpsManager.OPSTR_ACTIVATE_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(AppOpsManager.MODE_ALLOWED)); + } + + @Test + public void testSetPackageAuthorizationPlatformVpn() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(); + + assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_PLATFORM)); + verify(mAppOps) + .setMode( + eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(AppOpsManager.MODE_ALLOWED)); + } + + @Test + public void testSetPackageAuthorizationRevokeAuthorization() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(); + + assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_NONE)); + verify(mAppOps) + .setMode( + eq(AppOpsManager.OPSTR_ACTIVATE_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(AppOpsManager.MODE_IGNORED)); + verify(mAppOps) + .setMode( + eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(AppOpsManager.MODE_IGNORED)); + } + + private NetworkCallback triggerOnAvailableAndGetCallback() throws Exception { + final ArgumentCaptor networkCallbackCaptor = + ArgumentCaptor.forClass(NetworkCallback.class); + verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS)) + .requestNetwork(any(), networkCallbackCaptor.capture()); + + // onAvailable() will trigger onDefaultNetworkChanged(), so NetdUtils#setInterfaceUp will be + // invoked. Set the return value of INetd#interfaceGetCfg to prevent NullPointerException. + final InterfaceConfigurationParcel config = new InterfaceConfigurationParcel(); + config.flags = new String[] {IF_STATE_DOWN}; + when(mNetd.interfaceGetCfg(anyString())).thenReturn(config); + final NetworkCallback cb = networkCallbackCaptor.getValue(); + cb.onAvailable(TEST_NETWORK); + return cb; + } + + private void verifyInterfaceSetCfgWithFlags(String flag) throws Exception { + // Add a timeout for waiting for interfaceSetCfg to be called. + verify(mNetd, timeout(TEST_TIMEOUT_MS)).interfaceSetCfg(argThat( + config -> Arrays.asList(config.flags).contains(flag))); + } + + @Test + public void testStartPlatformVpnAuthenticationFailed() throws Exception { + final ArgumentCaptor captor = + ArgumentCaptor.forClass(IkeSessionCallback.class); + final IkeProtocolException exception = mock(IkeProtocolException.class); + when(exception.getErrorType()) + .thenReturn(IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED); + + final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), (mVpnProfile)); + final NetworkCallback cb = triggerOnAvailableAndGetCallback(); + + verifyInterfaceSetCfgWithFlags(IF_STATE_UP); + + // Wait for createIkeSession() to be called before proceeding in order to ensure consistent + // state + verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS)) + .createIkeSession(any(), any(), any(), any(), captor.capture(), any()); + final IkeSessionCallback ikeCb = captor.getValue(); + ikeCb.onClosedExceptionally(exception); + + verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS)).unregisterNetworkCallback(eq(cb)); + assertEquals(LegacyVpnInfo.STATE_FAILED, vpn.getLegacyVpnInfo().state); + } + + @Test + public void testStartPlatformVpnIllegalArgumentExceptionInSetup() throws Exception { + when(mIkev2SessionCreator.createIkeSession(any(), any(), any(), any(), any(), any())) + .thenThrow(new IllegalArgumentException()); + final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), mVpnProfile); + final NetworkCallback cb = triggerOnAvailableAndGetCallback(); + + verifyInterfaceSetCfgWithFlags(IF_STATE_UP); + + // Wait for createIkeSession() to be called before proceeding in order to ensure consistent + // state + verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS)).unregisterNetworkCallback(eq(cb)); + assertEquals(LegacyVpnInfo.STATE_FAILED, vpn.getLegacyVpnInfo().state); + } + + private void setAndVerifyAlwaysOnPackage(Vpn vpn, int uid, boolean lockdownEnabled) { + assertTrue(vpn.setAlwaysOnPackage(TEST_VPN_PKG, lockdownEnabled, null)); + + verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); + verify(mAppOps).setMode( + eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), eq(uid), eq(TEST_VPN_PKG), + eq(AppOpsManager.MODE_ALLOWED)); + + verify(mSystemServices).settingsSecurePutStringForUser( + eq(Settings.Secure.ALWAYS_ON_VPN_APP), eq(TEST_VPN_PKG), eq(primaryUser.id)); + verify(mSystemServices).settingsSecurePutIntForUser( + eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN), eq(lockdownEnabled ? 1 : 0), + eq(primaryUser.id)); + verify(mSystemServices).settingsSecurePutStringForUser( + eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST), eq(""), eq(primaryUser.id)); + } + + @Test + public void testSetAndStartAlwaysOnVpn() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + setMockedUsers(primaryUser); + + // UID checks must return a different UID; otherwise it'll be treated as already prepared. + final int uid = Process.myUid() + 1; + when(mPackageManager.getPackageUidAsUser(eq(TEST_VPN_PKG), anyInt())) + .thenReturn(uid); + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(mVpnProfile.encode()); + + setAndVerifyAlwaysOnPackage(vpn, uid, false); + assertTrue(vpn.startAlwaysOnVpn()); + + // TODO: Test the Ikev2VpnRunner started up properly. Relies on utility methods added in + // a subsequent CL. + } + + private Vpn startLegacyVpn(final Vpn vpn, final VpnProfile vpnProfile) throws Exception { + setMockedUsers(primaryUser); + + // Dummy egress interface + final LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(EGRESS_IFACE); + + final RouteInfo defaultRoute = new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), + InetAddresses.parseNumericAddress("192.0.2.0"), EGRESS_IFACE); + lp.addRoute(defaultRoute); + + vpn.startLegacyVpn(vpnProfile, EGRESS_NETWORK, lp); + return vpn; + } + + @Test + public void testStartPlatformVpn() throws Exception { + startLegacyVpn(createVpn(primaryUser.id), mVpnProfile); + // TODO: Test the Ikev2VpnRunner started up properly. Relies on utility methods added in + // a subsequent patch. + } + + @Test + public void testStartRacoonNumericAddress() throws Exception { + startRacoon("1.2.3.4", "1.2.3.4"); + } + + @Test + public void testStartRacoonHostname() throws Exception { + startRacoon("hostname", "5.6.7.8"); // address returned by deps.resolve + } + + private void assertTransportInfoMatches(NetworkCapabilities nc, int type) { + assertNotNull(nc); + VpnTransportInfo ti = (VpnTransportInfo) nc.getTransportInfo(); + assertNotNull(ti); + assertEquals(type, ti.getType()); + } + + public void startRacoon(final String serverAddr, final String expectedAddr) + throws Exception { + final ConditionVariable legacyRunnerReady = new ConditionVariable(); + final VpnProfile profile = new VpnProfile("testProfile" /* key */); + profile.type = VpnProfile.TYPE_L2TP_IPSEC_PSK; + profile.name = "testProfileName"; + profile.username = "userName"; + profile.password = "thePassword"; + profile.server = serverAddr; + profile.ipsecIdentifier = "id"; + profile.ipsecSecret = "secret"; + profile.l2tpSecret = "l2tpsecret"; + + when(mConnectivityManager.getAllNetworks()) + .thenReturn(new Network[] { new Network(101) }); + + when(mConnectivityManager.registerNetworkAgent(any(), any(), any(), any(), + any(), any(), anyInt())).thenAnswer(invocation -> { + // The runner has registered an agent and is now ready. + legacyRunnerReady.open(); + return new Network(102); + }); + final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), profile); + final TestDeps deps = (TestDeps) vpn.mDeps; + try { + // udppsk and 1701 are the values for TYPE_L2TP_IPSEC_PSK + assertArrayEquals( + new String[] { EGRESS_IFACE, expectedAddr, "udppsk", + profile.ipsecIdentifier, profile.ipsecSecret, "1701" }, + deps.racoonArgs.get(10, TimeUnit.SECONDS)); + // literal values are hardcoded in Vpn.java for mtpd args + assertArrayEquals( + new String[] { EGRESS_IFACE, "l2tp", expectedAddr, "1701", profile.l2tpSecret, + "name", profile.username, "password", profile.password, + "linkname", "vpn", "refuse-eap", "nodefaultroute", "usepeerdns", + "idle", "1800", "mtu", "1270", "mru", "1270" }, + deps.mtpdArgs.get(10, TimeUnit.SECONDS)); + + // Now wait for the runner to be ready before testing for the route. + ArgumentCaptor lpCaptor = ArgumentCaptor.forClass(LinkProperties.class); + ArgumentCaptor ncCaptor = + ArgumentCaptor.forClass(NetworkCapabilities.class); + verify(mConnectivityManager, timeout(10_000)).registerNetworkAgent(any(), any(), + lpCaptor.capture(), ncCaptor.capture(), any(), any(), anyInt()); + + // In this test the expected address is always v4 so /32. + // Note that the interface needs to be specified because RouteInfo objects stored in + // LinkProperties objects always acquire the LinkProperties' interface. + final RouteInfo expectedRoute = new RouteInfo(new IpPrefix(expectedAddr + "/32"), + null, EGRESS_IFACE, RouteInfo.RTN_THROW); + final List actualRoutes = lpCaptor.getValue().getRoutes(); + assertTrue("Expected throw route (" + expectedRoute + ") not found in " + actualRoutes, + actualRoutes.contains(expectedRoute)); + + assertTransportInfoMatches(ncCaptor.getValue(), VpnManager.TYPE_VPN_LEGACY); + } finally { + // Now interrupt the thread, unblock the runner and clean up. + vpn.mVpnRunner.exitVpnRunner(); + deps.getStateFile().delete(); // set to delete on exit, but this deletes it earlier + vpn.mVpnRunner.join(10_000); // wait for up to 10s for the runner to die and cleanup + } + } + + private static final class TestDeps extends Vpn.Dependencies { + public final CompletableFuture racoonArgs = new CompletableFuture(); + public final CompletableFuture mtpdArgs = new CompletableFuture(); + public final File mStateFile; + + private final HashMap mRunningServices = new HashMap<>(); + + TestDeps() { + try { + mStateFile = File.createTempFile("vpnTest", ".tmp"); + mStateFile.deleteOnExit(); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean isCallerSystem() { + return true; + } + + @Override + public void startService(final String serviceName) { + mRunningServices.put(serviceName, true); + } + + @Override + public void stopService(final String serviceName) { + mRunningServices.put(serviceName, false); + } + + @Override + public boolean isServiceRunning(final String serviceName) { + return mRunningServices.getOrDefault(serviceName, false); + } + + @Override + public boolean isServiceStopped(final String serviceName) { + return !isServiceRunning(serviceName); + } + + @Override + public File getStateFile() { + return mStateFile; + } + + @Override + public PendingIntent getIntentForStatusPanel(Context context) { + return null; + } + + @Override + public void sendArgumentsToDaemon( + final String daemon, final LocalSocket socket, final String[] arguments, + final Vpn.RetryScheduler interruptChecker) throws IOException { + if ("racoon".equals(daemon)) { + racoonArgs.complete(arguments); + } else if ("mtpd".equals(daemon)) { + writeStateFile(arguments); + mtpdArgs.complete(arguments); + } else { + throw new UnsupportedOperationException("Unsupported daemon : " + daemon); + } + } + + private void writeStateFile(final String[] arguments) throws IOException { + mStateFile.delete(); + mStateFile.createNewFile(); + mStateFile.deleteOnExit(); + final BufferedWriter writer = new BufferedWriter( + new FileWriter(mStateFile, false /* append */)); + writer.write(EGRESS_IFACE); + writer.write("\n"); + // addresses + writer.write("10.0.0.1/24\n"); + // routes + writer.write("192.168.6.0/24\n"); + // dns servers + writer.write("192.168.6.1\n"); + // search domains + writer.write("vpn.searchdomains.com\n"); + // endpoint - intentionally empty + writer.write("\n"); + writer.flush(); + writer.close(); + } + + @Override + @NonNull + public InetAddress resolve(final String endpoint) { + try { + // If a numeric IP address, return it. + return InetAddress.parseNumericAddress(endpoint); + } catch (IllegalArgumentException e) { + // Otherwise, return some token IP to test for. + return InetAddress.parseNumericAddress("5.6.7.8"); + } + } + + @Override + public boolean isInterfacePresent(final Vpn vpn, final String iface) { + return true; + } + } + + /** + * Mock some methods of vpn object. + */ + private Vpn createVpn(@UserIdInt int userId) { + final Context asUserContext = mock(Context.class, AdditionalAnswers.delegatesTo(mContext)); + doReturn(UserHandle.of(userId)).when(asUserContext).getUser(); + when(mContext.createContextAsUser(eq(UserHandle.of(userId)), anyInt())) + .thenReturn(asUserContext); + final TestLooper testLooper = new TestLooper(); + final Vpn vpn = new Vpn(testLooper.getLooper(), mContext, new TestDeps(), mNetService, + mNetd, userId, mVpnProfileStore, mSystemServices, mIkev2SessionCreator); + verify(mConnectivityManager, times(1)).registerNetworkProvider(argThat( + provider -> provider.getName().contains("VpnNetworkProvider") + )); + return vpn; + } + + /** + * Populate {@link #mUserManager} with a list of fake users. + */ + private void setMockedUsers(UserInfo... users) { + final Map userMap = new ArrayMap<>(); + for (UserInfo user : users) { + userMap.put(user.id, user); + } + + /** + * @see UserManagerService#getUsers(boolean) + */ + doAnswer(invocation -> { + final ArrayList result = new ArrayList<>(users.length); + for (UserInfo ui : users) { + if (ui.isEnabled() && !ui.partial) { + result.add(ui); + } + } + return result; + }).when(mUserManager).getAliveUsers(); + + doAnswer(invocation -> { + final int id = (int) invocation.getArguments()[0]; + return userMap.get(id); + }).when(mUserManager).getUserInfo(anyInt()); + } + + /** + * Populate {@link #mPackageManager} with a fake packageName-to-UID mapping. + */ + private void setMockedPackages(final Map packages) { + try { + doAnswer(invocation -> { + final String appName = (String) invocation.getArguments()[0]; + final int userId = (int) invocation.getArguments()[1]; + Integer appId = packages.get(appName); + if (appId == null) throw new PackageManager.NameNotFoundException(appName); + return UserHandle.getUid(userId, appId); + }).when(mPackageManager).getPackageUidAsUser(anyString(), anyInt()); + } catch (Exception e) { + } + } + + private void setMockedNetworks(final Map networks) { + doAnswer(invocation -> { + final Network network = (Network) invocation.getArguments()[0]; + return networks.get(network); + }).when(mConnectivityManager).getNetworkCapabilities(any()); + } + + // Need multiple copies of this, but Java's Stream objects can't be reused or + // duplicated. + private Stream publicIpV4Routes() { + return Stream.of( + "0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", + "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", + "172.0.0.0/12", "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", + "173.0.0.0/8", "174.0.0.0/7", "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", + "192.160.0.0/13", "192.169.0.0/16", "192.170.0.0/15", "192.172.0.0/14", + "192.176.0.0/12", "192.192.0.0/10", "193.0.0.0/8", "194.0.0.0/7", + "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"); + } + + private Stream publicIpV6Routes() { + return Stream.of( + "::/1", "8000::/2", "c000::/3", "e000::/4", "f000::/5", "f800::/6", + "fe00::/8", "2605:ef80:e:af1d::/64"); + } +} diff --git a/tests/unit/java/com/android/server/net/NetworkStatsAccessTest.java b/tests/unit/java/com/android/server/net/NetworkStatsAccessTest.java new file mode 100644 index 0000000000..8b730af769 --- /dev/null +++ b/tests/unit/java/com/android/server/net/NetworkStatsAccessTest.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2015 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.net; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +import android.Manifest; +import android.Manifest.permission; +import android.app.AppOpsManager; +import android.app.admin.DevicePolicyManagerInternal; +import android.content.Context; +import android.content.pm.PackageManager; +import android.telephony.TelephonyManager; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.LocalServices; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkStatsAccessTest { + private static final String TEST_PKG = "com.example.test"; + private static final int TEST_UID = 12345; + + @Mock private Context mContext; + @Mock private DevicePolicyManagerInternal mDpmi; + @Mock private TelephonyManager mTm; + @Mock private AppOpsManager mAppOps; + + // Hold the real service so we can restore it when tearing down the test. + private DevicePolicyManagerInternal mSystemDpmi; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mSystemDpmi = LocalServices.getService(DevicePolicyManagerInternal.class); + LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class); + LocalServices.addService(DevicePolicyManagerInternal.class, mDpmi); + + when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTm); + when(mContext.getSystemService(Context.APP_OPS_SERVICE)).thenReturn(mAppOps); + } + + @After + public void tearDown() throws Exception { + LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class); + LocalServices.addService(DevicePolicyManagerInternal.class, mSystemDpmi); + } + + @Test + public void testCheckAccessLevel_hasCarrierPrivileges() throws Exception { + setHasCarrierPrivileges(true); + setIsDeviceOwner(false); + setIsProfileOwner(false); + setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false); + setHasReadHistoryPermission(false); + assertEquals(NetworkStatsAccess.Level.DEVICE, + NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG)); + } + + @Test + public void testCheckAccessLevel_isDeviceOwner() throws Exception { + setHasCarrierPrivileges(false); + setIsDeviceOwner(true); + setIsProfileOwner(false); + setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false); + setHasReadHistoryPermission(false); + assertEquals(NetworkStatsAccess.Level.DEVICE, + NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG)); + } + + @Test + public void testCheckAccessLevel_isProfileOwner() throws Exception { + setHasCarrierPrivileges(false); + setIsDeviceOwner(false); + setIsProfileOwner(true); + setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false); + setHasReadHistoryPermission(false); + assertEquals(NetworkStatsAccess.Level.USER, + NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG)); + } + + @Test + public void testCheckAccessLevel_hasAppOpsBitAllowed() throws Exception { + setHasCarrierPrivileges(false); + setIsDeviceOwner(false); + setIsProfileOwner(true); + setHasAppOpsPermission(AppOpsManager.MODE_ALLOWED, false); + setHasReadHistoryPermission(false); + assertEquals(NetworkStatsAccess.Level.DEVICESUMMARY, + NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG)); + } + + @Test + public void testCheckAccessLevel_hasAppOpsBitDefault_grantedPermission() throws Exception { + setHasCarrierPrivileges(false); + setIsDeviceOwner(false); + setIsProfileOwner(true); + setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, true); + setHasReadHistoryPermission(false); + assertEquals(NetworkStatsAccess.Level.DEVICESUMMARY, + NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG)); + } + + @Test + public void testCheckAccessLevel_hasReadHistoryPermission() throws Exception { + setHasCarrierPrivileges(false); + setIsDeviceOwner(false); + setIsProfileOwner(true); + setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false); + setHasReadHistoryPermission(true); + assertEquals(NetworkStatsAccess.Level.DEVICESUMMARY, + NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG)); + } + + @Test + public void testCheckAccessLevel_deniedAppOpsBit() throws Exception { + setHasCarrierPrivileges(false); + setIsDeviceOwner(false); + setIsProfileOwner(false); + setHasAppOpsPermission(AppOpsManager.MODE_ERRORED, true); + setHasReadHistoryPermission(false); + assertEquals(NetworkStatsAccess.Level.DEFAULT, + NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG)); + } + + @Test + public void testCheckAccessLevel_deniedAppOpsBit_deniedPermission() throws Exception { + setHasCarrierPrivileges(false); + setIsDeviceOwner(false); + setIsProfileOwner(false); + setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false); + setHasReadHistoryPermission(false); + assertEquals(NetworkStatsAccess.Level.DEFAULT, + NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG)); + } + + private void setHasCarrierPrivileges(boolean hasPrivileges) { + when(mTm.checkCarrierPrivilegesForPackageAnyPhone(TEST_PKG)).thenReturn( + hasPrivileges ? TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS + : TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS); + } + + private void setIsDeviceOwner(boolean isOwner) { + when(mDpmi.isActiveDeviceOwner(TEST_UID)).thenReturn(isOwner); + } + + private void setIsProfileOwner(boolean isOwner) { + when(mDpmi.isActiveProfileOwner(TEST_UID)).thenReturn(isOwner); + } + + private void setHasAppOpsPermission(int appOpsMode, boolean hasPermission) { + when(mAppOps.noteOp(AppOpsManager.OP_GET_USAGE_STATS, TEST_UID, TEST_PKG)) + .thenReturn(appOpsMode); + when(mContext.checkCallingPermission(Manifest.permission.PACKAGE_USAGE_STATS)).thenReturn( + hasPermission ? PackageManager.PERMISSION_GRANTED + : PackageManager.PERMISSION_DENIED); + } + + private void setHasReadHistoryPermission(boolean hasPermission) { + when(mContext.checkCallingOrSelfPermission(permission.READ_NETWORK_USAGE_HISTORY)) + .thenReturn(hasPermission ? PackageManager.PERMISSION_GRANTED + : PackageManager.PERMISSION_DENIED); + } +} diff --git a/tests/unit/java/com/android/server/net/NetworkStatsBaseTest.java b/tests/unit/java/com/android/server/net/NetworkStatsBaseTest.java new file mode 100644 index 0000000000..a058a466a4 --- /dev/null +++ b/tests/unit/java/com/android/server/net/NetworkStatsBaseTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2011 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.net; + +import static android.net.NetworkStats.DEFAULT_NETWORK_ALL; +import static android.net.NetworkStats.DEFAULT_NETWORK_NO; +import static android.net.NetworkStats.DEFAULT_NETWORK_YES; +import static android.net.NetworkStats.METERED_ALL; +import static android.net.NetworkStats.METERED_NO; +import static android.net.NetworkStats.METERED_YES; +import static android.net.NetworkStats.ROAMING_ALL; +import static android.net.NetworkStats.ROAMING_NO; +import static android.net.NetworkStats.ROAMING_YES; +import static android.net.NetworkStats.SET_ALL; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.SET_FOREGROUND; +import static android.net.NetworkStats.TAG_NONE; + +import static org.junit.Assert.assertEquals; + +import android.net.NetworkStats; +import android.net.UnderlyingNetworkInfo; + +import java.util.Arrays; + +/** Superclass with utilities for NetworkStats(Service|Factory)Test */ +abstract class NetworkStatsBaseTest { + static final String TEST_IFACE = "test0"; + static final String TEST_IFACE2 = "test1"; + static final String TUN_IFACE = "test_nss_tun0"; + static final String TUN_IFACE2 = "test_nss_tun1"; + + static final int UID_RED = 1001; + static final int UID_BLUE = 1002; + static final int UID_GREEN = 1003; + static final int UID_VPN = 1004; + + void assertValues(NetworkStats stats, String iface, int uid, long rxBytes, + long rxPackets, long txBytes, long txPackets) { + assertValues( + stats, iface, uid, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, + rxBytes, rxPackets, txBytes, txPackets, 0); + } + + void assertValues(NetworkStats stats, String iface, int uid, int set, int tag, + int metered, int roaming, int defaultNetwork, long rxBytes, long rxPackets, + long txBytes, long txPackets, long operations) { + final NetworkStats.Entry entry = new NetworkStats.Entry(); + final int[] sets; + if (set == SET_ALL) { + sets = new int[] {SET_ALL, SET_DEFAULT, SET_FOREGROUND}; + } else { + sets = new int[] {set}; + } + + final int[] roamings; + if (roaming == ROAMING_ALL) { + roamings = new int[] {ROAMING_ALL, ROAMING_YES, ROAMING_NO}; + } else { + roamings = new int[] {roaming}; + } + + final int[] meterings; + if (metered == METERED_ALL) { + meterings = new int[] {METERED_ALL, METERED_YES, METERED_NO}; + } else { + meterings = new int[] {metered}; + } + + final int[] defaultNetworks; + if (defaultNetwork == DEFAULT_NETWORK_ALL) { + defaultNetworks = + new int[] {DEFAULT_NETWORK_ALL, DEFAULT_NETWORK_YES, DEFAULT_NETWORK_NO}; + } else { + defaultNetworks = new int[] {defaultNetwork}; + } + + for (int s : sets) { + for (int r : roamings) { + for (int m : meterings) { + for (int d : defaultNetworks) { + final int i = stats.findIndex(iface, uid, s, tag, m, r, d); + if (i != -1) { + entry.add(stats.getValues(i, null)); + } + } + } + } + } + + assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes); + assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets); + assertEquals("unexpected txBytes", txBytes, entry.txBytes); + assertEquals("unexpected txPackets", txPackets, entry.txPackets); + assertEquals("unexpected operations", operations, entry.operations); + } + + static UnderlyingNetworkInfo createVpnInfo(String[] underlyingIfaces) { + return createVpnInfo(TUN_IFACE, underlyingIfaces); + } + + static UnderlyingNetworkInfo createVpnInfo(String vpnIface, String[] underlyingIfaces) { + return new UnderlyingNetworkInfo(UID_VPN, vpnIface, Arrays.asList(underlyingIfaces)); + } +} diff --git a/tests/unit/java/com/android/server/net/NetworkStatsCollectionTest.java b/tests/unit/java/com/android/server/net/NetworkStatsCollectionTest.java new file mode 100644 index 0000000000..505ff9b6a3 --- /dev/null +++ b/tests/unit/java/com/android/server/net/NetworkStatsCollectionTest.java @@ -0,0 +1,594 @@ +/* + * Copyright (C) 2012 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.net; + +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.NetworkIdentity.OEM_NONE; +import static android.net.NetworkStats.SET_ALL; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkStats.UID_ALL; +import static android.net.NetworkStatsHistory.FIELD_ALL; +import static android.net.NetworkTemplate.buildTemplateMobileAll; +import static android.os.Process.myUid; +import static android.text.format.DateUtils.HOUR_IN_MILLIS; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; + +import static com.android.internal.net.NetworkUtilsInternal.multiplySafeByRational; +import static com.android.testutils.MiscAsserts.assertThrows; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.net.NetworkIdentity; +import android.net.NetworkStats; +import android.net.NetworkStatsHistory; +import android.net.NetworkTemplate; +import android.os.Process; +import android.os.UserHandle; +import android.telephony.SubscriptionPlan; +import android.telephony.TelephonyManager; +import android.text.format.DateUtils; +import android.util.RecurrenceRule; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.frameworks.tests.net.R; + +import libcore.io.IoUtils; +import libcore.io.Streams; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * Tests for {@link NetworkStatsCollection}. + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkStatsCollectionTest { + + private static final String TEST_FILE = "test.bin"; + private static final String TEST_IMSI = "310260000000000"; + + private static final long TIME_A = 1326088800000L; // UTC: Monday 9th January 2012 06:00:00 AM + private static final long TIME_B = 1326110400000L; // UTC: Monday 9th January 2012 12:00:00 PM + private static final long TIME_C = 1326132000000L; // UTC: Monday 9th January 2012 06:00:00 PM + + private static Clock sOriginalClock; + + @Before + public void setUp() throws Exception { + sOriginalClock = RecurrenceRule.sClock; + // ignore any device overlay while testing + NetworkTemplate.forceAllNetworkTypes(); + } + + @After + public void tearDown() throws Exception { + RecurrenceRule.sClock = sOriginalClock; + NetworkTemplate.resetForceAllNetworkTypes(); + } + + private void setClock(Instant instant) { + RecurrenceRule.sClock = Clock.fixed(instant, ZoneId.systemDefault()); + } + + @Test + public void testReadLegacyNetwork() throws Exception { + final File testFile = + new File(InstrumentationRegistry.getContext().getFilesDir(), TEST_FILE); + stageFile(R.raw.netstats_v1, testFile); + + final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS); + collection.readLegacyNetwork(testFile); + + // verify that history read correctly + assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), + 636016770L, 709306L, 88038768L, 518836L, NetworkStatsAccess.Level.DEVICE); + + // now export into a unified format + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + collection.write(bos); + + // clear structure completely + collection.reset(); + assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), + 0L, 0L, 0L, 0L, NetworkStatsAccess.Level.DEVICE); + + // and read back into structure, verifying that totals are same + collection.read(new ByteArrayInputStream(bos.toByteArray())); + assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), + 636016770L, 709306L, 88038768L, 518836L, NetworkStatsAccess.Level.DEVICE); + } + + @Test + public void testReadLegacyUid() throws Exception { + final File testFile = + new File(InstrumentationRegistry.getContext().getFilesDir(), TEST_FILE); + stageFile(R.raw.netstats_uid_v4, testFile); + + final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS); + collection.readLegacyUid(testFile, false); + + // verify that history read correctly + assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), + 637076152L, 711413L, 88343717L, 521022L, NetworkStatsAccess.Level.DEVICE); + + // now export into a unified format + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + collection.write(bos); + + // clear structure completely + collection.reset(); + assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), + 0L, 0L, 0L, 0L, NetworkStatsAccess.Level.DEVICE); + + // and read back into structure, verifying that totals are same + collection.read(new ByteArrayInputStream(bos.toByteArray())); + assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), + 637076152L, 711413L, 88343717L, 521022L, NetworkStatsAccess.Level.DEVICE); + } + + @Test + public void testReadLegacyUidTags() throws Exception { + final File testFile = + new File(InstrumentationRegistry.getContext().getFilesDir(), TEST_FILE); + stageFile(R.raw.netstats_uid_v4, testFile); + + final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS); + collection.readLegacyUid(testFile, true); + + // verify that history read correctly + assertSummaryTotalIncludingTags(collection, buildTemplateMobileAll(TEST_IMSI), + 77017831L, 100995L, 35436758L, 92344L); + + // now export into a unified format + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + collection.write(bos); + + // clear structure completely + collection.reset(); + assertSummaryTotalIncludingTags(collection, buildTemplateMobileAll(TEST_IMSI), + 0L, 0L, 0L, 0L); + + // and read back into structure, verifying that totals are same + collection.read(new ByteArrayInputStream(bos.toByteArray())); + assertSummaryTotalIncludingTags(collection, buildTemplateMobileAll(TEST_IMSI), + 77017831L, 100995L, 35436758L, 92344L); + } + + @Test + public void testStartEndAtomicBuckets() throws Exception { + final NetworkStatsCollection collection = new NetworkStatsCollection(HOUR_IN_MILLIS); + + // record empty data straddling between buckets + final NetworkStats.Entry entry = new NetworkStats.Entry(); + entry.rxBytes = 32; + collection.recordData(null, UID_ALL, SET_DEFAULT, TAG_NONE, 30 * MINUTE_IN_MILLIS, + 90 * MINUTE_IN_MILLIS, entry); + + // assert that we report boundary in atomic buckets + assertEquals(0, collection.getStartMillis()); + assertEquals(2 * HOUR_IN_MILLIS, collection.getEndMillis()); + } + + @Test + public void testAccessLevels() throws Exception { + final NetworkStatsCollection collection = new NetworkStatsCollection(HOUR_IN_MILLIS); + final NetworkStats.Entry entry = new NetworkStats.Entry(); + final NetworkIdentitySet identSet = new NetworkIdentitySet(); + identSet.add(new NetworkIdentity(TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_UNKNOWN, + TEST_IMSI, null, false, true, true, OEM_NONE)); + + int myUid = Process.myUid(); + int otherUidInSameUser = Process.myUid() + 1; + int uidInDifferentUser = Process.myUid() + UserHandle.PER_USER_RANGE; + + // Record one entry for the current UID. + entry.rxBytes = 32; + collection.recordData(identSet, myUid, SET_DEFAULT, TAG_NONE, 0, 60 * MINUTE_IN_MILLIS, + entry); + + // Record one entry for another UID in this user. + entry.rxBytes = 64; + collection.recordData(identSet, otherUidInSameUser, SET_DEFAULT, TAG_NONE, 0, + 60 * MINUTE_IN_MILLIS, entry); + + // Record one entry for the system UID. + entry.rxBytes = 128; + collection.recordData(identSet, Process.SYSTEM_UID, SET_DEFAULT, TAG_NONE, 0, + 60 * MINUTE_IN_MILLIS, entry); + + // Record one entry for a UID in a different user. + entry.rxBytes = 256; + collection.recordData(identSet, uidInDifferentUser, SET_DEFAULT, TAG_NONE, 0, + 60 * MINUTE_IN_MILLIS, entry); + + // Verify the set of relevant UIDs for each access level. + assertArrayEquals(new int[] { myUid }, + collection.getRelevantUids(NetworkStatsAccess.Level.DEFAULT)); + assertArrayEquals(new int[] { Process.SYSTEM_UID, myUid, otherUidInSameUser }, + collection.getRelevantUids(NetworkStatsAccess.Level.USER)); + assertArrayEquals( + new int[] { Process.SYSTEM_UID, myUid, otherUidInSameUser, uidInDifferentUser }, + collection.getRelevantUids(NetworkStatsAccess.Level.DEVICE)); + + // Verify security check in getHistory. + assertNotNull(collection.getHistory(buildTemplateMobileAll(TEST_IMSI), null, myUid, SET_DEFAULT, + TAG_NONE, 0, 0L, 0L, NetworkStatsAccess.Level.DEFAULT, myUid)); + try { + collection.getHistory(buildTemplateMobileAll(TEST_IMSI), null, otherUidInSameUser, + SET_DEFAULT, TAG_NONE, 0, 0L, 0L, NetworkStatsAccess.Level.DEFAULT, myUid); + fail("Should have thrown SecurityException for accessing different UID"); + } catch (SecurityException e) { + // expected + } + + // Verify appropriate aggregation in getSummary. + assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), 32, 0, 0, 0, + NetworkStatsAccess.Level.DEFAULT); + assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), 32 + 64 + 128, 0, 0, 0, + NetworkStatsAccess.Level.USER); + assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), 32 + 64 + 128 + 256, 0, 0, + 0, NetworkStatsAccess.Level.DEVICE); + } + + @Test + public void testAugmentPlan() throws Exception { + final File testFile = + new File(InstrumentationRegistry.getContext().getFilesDir(), TEST_FILE); + stageFile(R.raw.netstats_v1, testFile); + + final NetworkStatsCollection emptyCollection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS); + final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS); + collection.readLegacyNetwork(testFile); + + // We're in the future, but not that far off + setClock(Instant.parse("2012-06-01T00:00:00.00Z")); + + // Test a bunch of plans that should result in no augmentation + final List plans = new ArrayList<>(); + + // No plan + plans.add(null); + // No usage anchor + plans.add(SubscriptionPlan.Builder + .createRecurringMonthly(ZonedDateTime.parse("2011-01-14T00:00:00.00Z")).build()); + // Usage anchor far in past + plans.add(SubscriptionPlan.Builder + .createRecurringMonthly(ZonedDateTime.parse("2011-01-14T00:00:00.00Z")) + .setDataUsage(1000L, TIME_A - DateUtils.YEAR_IN_MILLIS).build()); + // Usage anchor far in future + plans.add(SubscriptionPlan.Builder + .createRecurringMonthly(ZonedDateTime.parse("2011-01-14T00:00:00.00Z")) + .setDataUsage(1000L, TIME_A + DateUtils.YEAR_IN_MILLIS).build()); + // Usage anchor near but outside cycle + plans.add(SubscriptionPlan.Builder + .createNonrecurring(ZonedDateTime.parse("2012-01-09T09:00:00.00Z"), + ZonedDateTime.parse("2012-01-09T15:00:00.00Z")) + .setDataUsage(1000L, TIME_C).build()); + + for (SubscriptionPlan plan : plans) { + int i; + NetworkStatsHistory history; + + // Empty collection should be untouched + history = getHistory(emptyCollection, plan, TIME_A, TIME_C); + assertEquals(0L, history.getTotalBytes()); + + // Normal collection should be untouched + history = getHistory(collection, plan, TIME_A, TIME_C); i = 0; + assertEntry(100647, 197, 23649, 185, history.getValues(i++, null)); + assertEntry(100647, 196, 23648, 185, history.getValues(i++, null)); + assertEntry(18323, 76, 15032, 76, history.getValues(i++, null)); + assertEntry(18322, 75, 15031, 75, history.getValues(i++, null)); + assertEntry(527798, 761, 78570, 652, history.getValues(i++, null)); + assertEntry(527797, 760, 78570, 651, history.getValues(i++, null)); + assertEntry(10747, 50, 16839, 55, history.getValues(i++, null)); + assertEntry(10747, 49, 16837, 54, history.getValues(i++, null)); + assertEntry(89191, 151, 18021, 140, history.getValues(i++, null)); + assertEntry(89190, 150, 18020, 139, history.getValues(i++, null)); + assertEntry(3821, 23, 4525, 26, history.getValues(i++, null)); + assertEntry(3820, 21, 4524, 26, history.getValues(i++, null)); + assertEntry(91686, 159, 18576, 146, history.getValues(i++, null)); + assertEntry(91685, 159, 18574, 146, history.getValues(i++, null)); + assertEntry(8289, 36, 6864, 39, history.getValues(i++, null)); + assertEntry(8289, 34, 6862, 37, history.getValues(i++, null)); + assertEntry(113914, 174, 18364, 157, history.getValues(i++, null)); + assertEntry(113913, 173, 18364, 157, history.getValues(i++, null)); + assertEntry(11378, 49, 9261, 50, history.getValues(i++, null)); + assertEntry(11377, 48, 9261, 48, history.getValues(i++, null)); + assertEntry(201766, 328, 41808, 291, history.getValues(i++, null)); + assertEntry(201764, 328, 41807, 290, history.getValues(i++, null)); + assertEntry(106106, 219, 39918, 202, history.getValues(i++, null)); + assertEntry(106105, 216, 39916, 200, history.getValues(i++, null)); + assertEquals(history.size(), i); + + // Slice from middle should be untouched + history = getHistory(collection, plan, TIME_B - HOUR_IN_MILLIS, + TIME_B + HOUR_IN_MILLIS); i = 0; + assertEntry(3821, 23, 4525, 26, history.getValues(i++, null)); + assertEntry(3820, 21, 4524, 26, history.getValues(i++, null)); + assertEntry(91686, 159, 18576, 146, history.getValues(i++, null)); + assertEntry(91685, 159, 18574, 146, history.getValues(i++, null)); + assertEquals(history.size(), i); + } + + // Lower anchor in the middle of plan + { + int i; + NetworkStatsHistory history; + + final SubscriptionPlan plan = SubscriptionPlan.Builder + .createNonrecurring(ZonedDateTime.parse("2012-01-09T09:00:00.00Z"), + ZonedDateTime.parse("2012-01-09T15:00:00.00Z")) + .setDataUsage(200000L, TIME_B).build(); + + // Empty collection should be augmented + history = getHistory(emptyCollection, plan, TIME_A, TIME_C); + assertEquals(200000L, history.getTotalBytes()); + + // Normal collection should be augmented + history = getHistory(collection, plan, TIME_A, TIME_C); i = 0; + assertEntry(100647, 197, 23649, 185, history.getValues(i++, null)); + assertEntry(100647, 196, 23648, 185, history.getValues(i++, null)); + assertEntry(18323, 76, 15032, 76, history.getValues(i++, null)); + assertEntry(18322, 75, 15031, 75, history.getValues(i++, null)); + assertEntry(527798, 761, 78570, 652, history.getValues(i++, null)); + assertEntry(527797, 760, 78570, 651, history.getValues(i++, null)); + // Cycle point; start data normalization + assertEntry(7507, 0, 11763, 0, history.getValues(i++, null)); + assertEntry(7507, 0, 11762, 0, history.getValues(i++, null)); + assertEntry(62309, 0, 12589, 0, history.getValues(i++, null)); + assertEntry(62309, 0, 12588, 0, history.getValues(i++, null)); + assertEntry(2669, 0, 3161, 0, history.getValues(i++, null)); + assertEntry(2668, 0, 3160, 0, history.getValues(i++, null)); + // Anchor point; end data normalization + assertEntry(91686, 159, 18576, 146, history.getValues(i++, null)); + assertEntry(91685, 159, 18574, 146, history.getValues(i++, null)); + assertEntry(8289, 36, 6864, 39, history.getValues(i++, null)); + assertEntry(8289, 34, 6862, 37, history.getValues(i++, null)); + assertEntry(113914, 174, 18364, 157, history.getValues(i++, null)); + assertEntry(113913, 173, 18364, 157, history.getValues(i++, null)); + // Cycle point + assertEntry(11378, 49, 9261, 50, history.getValues(i++, null)); + assertEntry(11377, 48, 9261, 48, history.getValues(i++, null)); + assertEntry(201766, 328, 41808, 291, history.getValues(i++, null)); + assertEntry(201764, 328, 41807, 290, history.getValues(i++, null)); + assertEntry(106106, 219, 39918, 202, history.getValues(i++, null)); + assertEntry(106105, 216, 39916, 200, history.getValues(i++, null)); + assertEquals(history.size(), i); + + // Slice from middle should be augmented + history = getHistory(collection, plan, TIME_B - HOUR_IN_MILLIS, + TIME_B + HOUR_IN_MILLIS); i = 0; + assertEntry(2669, 0, 3161, 0, history.getValues(i++, null)); + assertEntry(2668, 0, 3160, 0, history.getValues(i++, null)); + assertEntry(91686, 159, 18576, 146, history.getValues(i++, null)); + assertEntry(91685, 159, 18574, 146, history.getValues(i++, null)); + assertEquals(history.size(), i); + } + + // Higher anchor in the middle of plan + { + int i; + NetworkStatsHistory history; + + final SubscriptionPlan plan = SubscriptionPlan.Builder + .createNonrecurring(ZonedDateTime.parse("2012-01-09T09:00:00.00Z"), + ZonedDateTime.parse("2012-01-09T15:00:00.00Z")) + .setDataUsage(400000L, TIME_B + MINUTE_IN_MILLIS).build(); + + // Empty collection should be augmented + history = getHistory(emptyCollection, plan, TIME_A, TIME_C); + assertEquals(400000L, history.getTotalBytes()); + + // Normal collection should be augmented + history = getHistory(collection, plan, TIME_A, TIME_C); i = 0; + assertEntry(100647, 197, 23649, 185, history.getValues(i++, null)); + assertEntry(100647, 196, 23648, 185, history.getValues(i++, null)); + assertEntry(18323, 76, 15032, 76, history.getValues(i++, null)); + assertEntry(18322, 75, 15031, 75, history.getValues(i++, null)); + assertEntry(527798, 761, 78570, 652, history.getValues(i++, null)); + assertEntry(527797, 760, 78570, 651, history.getValues(i++, null)); + // Cycle point; start data normalization + assertEntry(15015, 0, 23527, 0, history.getValues(i++, null)); + assertEntry(15015, 0, 23524, 0, history.getValues(i++, null)); + assertEntry(124619, 0, 25179, 0, history.getValues(i++, null)); + assertEntry(124618, 0, 25177, 0, history.getValues(i++, null)); + assertEntry(5338, 0, 6322, 0, history.getValues(i++, null)); + assertEntry(5337, 0, 6320, 0, history.getValues(i++, null)); + // Anchor point; end data normalization + assertEntry(91686, 159, 18576, 146, history.getValues(i++, null)); + assertEntry(91685, 159, 18574, 146, history.getValues(i++, null)); + assertEntry(8289, 36, 6864, 39, history.getValues(i++, null)); + assertEntry(8289, 34, 6862, 37, history.getValues(i++, null)); + assertEntry(113914, 174, 18364, 157, history.getValues(i++, null)); + assertEntry(113913, 173, 18364, 157, history.getValues(i++, null)); + // Cycle point + assertEntry(11378, 49, 9261, 50, history.getValues(i++, null)); + assertEntry(11377, 48, 9261, 48, history.getValues(i++, null)); + assertEntry(201766, 328, 41808, 291, history.getValues(i++, null)); + assertEntry(201764, 328, 41807, 290, history.getValues(i++, null)); + assertEntry(106106, 219, 39918, 202, history.getValues(i++, null)); + assertEntry(106105, 216, 39916, 200, history.getValues(i++, null)); + + // Slice from middle should be augmented + history = getHistory(collection, plan, TIME_B - HOUR_IN_MILLIS, + TIME_B + HOUR_IN_MILLIS); i = 0; + assertEntry(5338, 0, 6322, 0, history.getValues(i++, null)); + assertEntry(5337, 0, 6320, 0, history.getValues(i++, null)); + assertEntry(91686, 159, 18576, 146, history.getValues(i++, null)); + assertEntry(91685, 159, 18574, 146, history.getValues(i++, null)); + assertEquals(history.size(), i); + } + } + + @Test + public void testAugmentPlanGigantic() throws Exception { + // We're in the future, but not that far off + setClock(Instant.parse("2012-06-01T00:00:00.00Z")); + + // Create a simple history with a ton of measured usage + final NetworkStatsCollection large = new NetworkStatsCollection(HOUR_IN_MILLIS); + final NetworkIdentitySet ident = new NetworkIdentitySet(); + ident.add(new NetworkIdentity(ConnectivityManager.TYPE_MOBILE, -1, TEST_IMSI, null, + false, true, true, OEM_NONE)); + large.recordData(ident, UID_ALL, SET_ALL, TAG_NONE, TIME_A, TIME_B, + new NetworkStats.Entry(12_730_893_164L, 1, 0, 0, 0)); + + // Verify untouched total + assertEquals(12_730_893_164L, getHistory(large, null, TIME_A, TIME_C).getTotalBytes()); + + // Verify anchor that might cause overflows + final SubscriptionPlan plan = SubscriptionPlan.Builder + .createRecurringMonthly(ZonedDateTime.parse("2012-01-09T00:00:00.00Z")) + .setDataUsage(4_939_212_390L, TIME_B).build(); + assertEquals(4_939_212_386L, getHistory(large, plan, TIME_A, TIME_C).getTotalBytes()); + } + + @Test + public void testRounding() throws Exception { + final NetworkStatsCollection coll = new NetworkStatsCollection(HOUR_IN_MILLIS); + + // Special values should remain unchanged + for (long time : new long[] { + Long.MIN_VALUE, Long.MAX_VALUE, SubscriptionPlan.TIME_UNKNOWN + }) { + assertEquals(time, coll.roundUp(time)); + assertEquals(time, coll.roundDown(time)); + } + + assertEquals(TIME_A, coll.roundUp(TIME_A)); + assertEquals(TIME_A, coll.roundDown(TIME_A)); + + assertEquals(TIME_A + HOUR_IN_MILLIS, coll.roundUp(TIME_A + 1)); + assertEquals(TIME_A, coll.roundDown(TIME_A + 1)); + + assertEquals(TIME_A, coll.roundUp(TIME_A - 1)); + assertEquals(TIME_A - HOUR_IN_MILLIS, coll.roundDown(TIME_A - 1)); + } + + @Test + public void testMultiplySafeRational() { + assertEquals(25, multiplySafeByRational(50, 1, 2)); + assertEquals(100, multiplySafeByRational(50, 2, 1)); + + assertEquals(-10, multiplySafeByRational(30, -1, 3)); + assertEquals(0, multiplySafeByRational(30, 0, 3)); + assertEquals(10, multiplySafeByRational(30, 1, 3)); + assertEquals(20, multiplySafeByRational(30, 2, 3)); + assertEquals(30, multiplySafeByRational(30, 3, 3)); + assertEquals(40, multiplySafeByRational(30, 4, 3)); + + assertEquals(100_000_000_000L, + multiplySafeByRational(300_000_000_000L, 10_000_000_000L, 30_000_000_000L)); + assertEquals(100_000_000_010L, + multiplySafeByRational(300_000_000_000L, 10_000_000_001L, 30_000_000_000L)); + assertEquals(823_202_048L, + multiplySafeByRational(4_939_212_288L, 2_121_815_528L, 12_730_893_165L)); + + assertThrows(ArithmeticException.class, () -> multiplySafeByRational(30, 3, 0)); + } + + /** + * Copy a {@link Resources#openRawResource(int)} into {@link File} for + * testing purposes. + */ + private void stageFile(int rawId, File file) throws Exception { + new File(file.getParent()).mkdirs(); + InputStream in = null; + OutputStream out = null; + try { + in = InstrumentationRegistry.getContext().getResources().openRawResource(rawId); + out = new FileOutputStream(file); + Streams.copy(in, out); + } finally { + IoUtils.closeQuietly(in); + IoUtils.closeQuietly(out); + } + } + + private static NetworkStatsHistory getHistory(NetworkStatsCollection collection, + SubscriptionPlan augmentPlan, long start, long end) { + return collection.getHistory(buildTemplateMobileAll(TEST_IMSI), augmentPlan, UID_ALL, + SET_ALL, TAG_NONE, FIELD_ALL, start, end, NetworkStatsAccess.Level.DEVICE, myUid()); + } + + private static void assertSummaryTotal(NetworkStatsCollection collection, + NetworkTemplate template, long rxBytes, long rxPackets, long txBytes, long txPackets, + @NetworkStatsAccess.Level int accessLevel) { + final NetworkStats.Entry actual = collection.getSummary( + template, Long.MIN_VALUE, Long.MAX_VALUE, accessLevel, myUid()) + .getTotal(null); + assertEntry(rxBytes, rxPackets, txBytes, txPackets, actual); + } + + private static void assertSummaryTotalIncludingTags(NetworkStatsCollection collection, + NetworkTemplate template, long rxBytes, long rxPackets, long txBytes, long txPackets) { + final NetworkStats.Entry actual = collection.getSummary( + template, Long.MIN_VALUE, Long.MAX_VALUE, NetworkStatsAccess.Level.DEVICE, myUid()) + .getTotalIncludingTags(null); + assertEntry(rxBytes, rxPackets, txBytes, txPackets, actual); + } + + private static void assertEntry(long rxBytes, long rxPackets, long txBytes, long txPackets, + NetworkStats.Entry actual) { + assertEntry(new NetworkStats.Entry(rxBytes, rxPackets, txBytes, txPackets, 0L), actual); + } + + private static void assertEntry(long rxBytes, long rxPackets, long txBytes, long txPackets, + NetworkStatsHistory.Entry actual) { + assertEntry(new NetworkStats.Entry(rxBytes, rxPackets, txBytes, txPackets, 0L), actual); + } + + private static void assertEntry(NetworkStats.Entry expected, + NetworkStatsHistory.Entry actual) { + assertEntry(expected, new NetworkStats.Entry(actual.rxBytes, actual.rxPackets, + actual.txBytes, actual.txPackets, 0L)); + } + + private static void assertEntry(NetworkStats.Entry expected, + NetworkStats.Entry actual) { + assertEquals("unexpected rxBytes", expected.rxBytes, actual.rxBytes); + assertEquals("unexpected rxPackets", expected.rxPackets, actual.rxPackets); + assertEquals("unexpected txBytes", expected.txBytes, actual.txBytes); + assertEquals("unexpected txPackets", expected.txPackets, actual.txPackets); + } +} diff --git a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java new file mode 100644 index 0000000000..f3ae9b051e --- /dev/null +++ b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java @@ -0,0 +1,578 @@ +/* + * Copyright (C) 2011 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.net; + +import static android.net.NetworkStats.DEFAULT_NETWORK_ALL; +import static android.net.NetworkStats.DEFAULT_NETWORK_NO; +import static android.net.NetworkStats.METERED_ALL; +import static android.net.NetworkStats.METERED_NO; +import static android.net.NetworkStats.ROAMING_ALL; +import static android.net.NetworkStats.ROAMING_NO; +import static android.net.NetworkStats.SET_ALL; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.SET_FOREGROUND; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkStats.UID_ALL; + +import static com.android.server.NetworkManagementSocketTagger.kernelToTag; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import android.content.res.Resources; +import android.net.NetworkStats; +import android.net.TrafficStats; +import android.net.UnderlyingNetworkInfo; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.frameworks.tests.net.R; + +import libcore.io.IoUtils; +import libcore.io.Streams; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.InputStream; +import java.io.OutputStream; + +/** Tests for {@link NetworkStatsFactory}. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkStatsFactoryTest extends NetworkStatsBaseTest { + private static final String CLAT_PREFIX = "v4-"; + + private File mTestProc; + private NetworkStatsFactory mFactory; + + @Before + public void setUp() throws Exception { + mTestProc = new File(InstrumentationRegistry.getContext().getFilesDir(), "proc"); + if (mTestProc.exists()) { + IoUtils.deleteContents(mTestProc); + } + + // The libandroid_servers which have the native method is not available to + // applications. So in order to have a test support native library, the native code + // related to networkStatsFactory is compiled to a minimal native library and loaded here. + System.loadLibrary("networkstatsfactorytestjni"); + mFactory = new NetworkStatsFactory(mTestProc, false); + mFactory.updateUnderlyingNetworkInfos(new UnderlyingNetworkInfo[0]); + } + + @After + public void tearDown() throws Exception { + mFactory = null; + + if (mTestProc.exists()) { + IoUtils.deleteContents(mTestProc); + } + } + + @Test + public void testNetworkStatsDetail() throws Exception { + final NetworkStats stats = parseDetailedStats(R.raw.xt_qtaguid_typical); + + assertEquals(70, stats.size()); + assertStatsEntry(stats, "wlan0", 0, SET_DEFAULT, 0x0, 18621L, 2898L); + assertStatsEntry(stats, "wlan0", 10011, SET_DEFAULT, 0x0, 35777L, 5718L); + assertStatsEntry(stats, "wlan0", 10021, SET_DEFAULT, 0x7fffff01, 562386L, 49228L); + assertStatsEntry(stats, "rmnet1", 10021, SET_DEFAULT, 0x30100000, 219110L, 227423L); + assertStatsEntry(stats, "rmnet2", 10001, SET_DEFAULT, 0x0, 1125899906842624L, 984L); + } + + @Test + public void testVpnRewriteTrafficThroughItself() throws Exception { + UnderlyingNetworkInfo[] underlyingNetworkInfos = + new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE})}; + mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); + + // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption + // overhead per packet): + // + // 1000 bytes (100 packets) were sent, and 2000 bytes (200 packets) were received by UID_RED + // over VPN. + // 500 bytes (50 packets) were sent, and 1000 bytes (100 packets) were received by UID_BLUE + // over VPN. + // + // VPN UID rewrites packets read from TUN back to TUN, plus some of its own traffic + + final NetworkStats tunStats = parseDetailedStats(R.raw.xt_qtaguid_vpn_rewrite_through_self); + + assertValues(tunStats, TUN_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, + DEFAULT_NETWORK_ALL, 2000L, 200L, 1000L, 100L, 0); + assertValues(tunStats, TUN_IFACE, UID_BLUE, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, + DEFAULT_NETWORK_ALL, 1000L, 100L, 500L, 50L, 0); + assertValues(tunStats, TUN_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, + DEFAULT_NETWORK_ALL, 0L, 0L, 1600L, 160L, 0); + + assertValues(tunStats, TEST_IFACE, UID_RED, 2000L, 200L, 1000L, 100L); + assertValues(tunStats, TEST_IFACE, UID_BLUE, 1000L, 100L, 500L, 50L); + assertValues(tunStats, TEST_IFACE, UID_VPN, 300L, 0L, 260L, 26L); + } + + @Test + public void testVpnWithClat() throws Exception { + final UnderlyingNetworkInfo[] underlyingNetworkInfos = new UnderlyingNetworkInfo[] { + createVpnInfo(new String[] {CLAT_PREFIX + TEST_IFACE})}; + mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); + mFactory.noteStackedIface(CLAT_PREFIX + TEST_IFACE, TEST_IFACE); + + // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption + // overhead per packet): + // 1000 bytes (100 packets) were sent, and 2000 bytes (200 packets) were received by UID_RED + // over VPN. + // 500 bytes (50 packets) were sent, and 1000 bytes (100 packets) were received by UID_BLUE + // over VPN. + // VPN sent 1650 bytes (150 packets), and received 3300 (300 packets) over v4-WiFi, and clat + // added 20 bytes per packet of extra overhead + // + // For 1650 bytes sent over v4-WiFi, 4650 bytes were actually sent over WiFi, which is + // expected to be split as follows: + // UID_RED: 1000 bytes, 100 packets + // UID_BLUE: 500 bytes, 50 packets + // UID_VPN: 3150 bytes, 0 packets + // + // For 3300 bytes received over v4-WiFi, 9300 bytes were actually sent over WiFi, which is + // expected to be split as follows: + // UID_RED: 2000 bytes, 200 packets + // UID_BLUE: 1000 bytes, 100 packets + // UID_VPN: 6300 bytes, 0 packets + final NetworkStats tunStats = parseDetailedStats(R.raw.xt_qtaguid_vpn_with_clat); + + assertValues(tunStats, CLAT_PREFIX + TEST_IFACE, UID_RED, 2000L, 200L, 1000, 100L); + assertValues(tunStats, CLAT_PREFIX + TEST_IFACE, UID_BLUE, 1000L, 100L, 500L, 50L); + assertValues(tunStats, CLAT_PREFIX + TEST_IFACE, UID_VPN, 6300L, 0L, 3150L, 0L); + } + + @Test + public void testVpnWithOneUnderlyingIface() throws Exception { + final UnderlyingNetworkInfo[] underlyingNetworkInfos = + new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE})}; + mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); + + // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption + // overhead per packet): + // 1000 bytes (100 packets) were sent, and 2000 bytes (200 packets) were received by UID_RED + // over VPN. + // 500 bytes (50 packets) were sent, and 1000 bytes (100 packets) were received by UID_BLUE + // over VPN. + // VPN sent 1650 bytes (150 packets), and received 3300 (300 packets) over WiFi. + // Of 1650 bytes sent over WiFi, expect 1000 bytes attributed to UID_RED, 500 bytes + // attributed to UID_BLUE, and 150 bytes attributed to UID_VPN. + // Of 3300 bytes received over WiFi, expect 2000 bytes attributed to UID_RED, 1000 bytes + // attributed to UID_BLUE, and 300 bytes attributed to UID_VPN. + final NetworkStats tunStats = parseDetailedStats(R.raw.xt_qtaguid_vpn_one_underlying); + + assertValues(tunStats, TEST_IFACE, UID_RED, 2000L, 200L, 1000L, 100L); + assertValues(tunStats, TEST_IFACE, UID_BLUE, 1000L, 100L, 500L, 50L); + assertValues(tunStats, TEST_IFACE, UID_VPN, 300L, 0L, 150L, 0L); + } + + @Test + public void testVpnWithOneUnderlyingIfaceAndOwnTraffic() throws Exception { + // WiFi network is connected and VPN is using WiFi (which has TEST_IFACE). + final UnderlyingNetworkInfo[] underlyingNetworkInfos = + new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE})}; + mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); + + // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption + // overhead per packet): + // 1000 bytes (100 packets) were sent, and 2000 bytes (200 packets) were received by UID_RED + // over VPN. + // 500 bytes (50 packets) were sent, and 1000 bytes (100 packets) were received by UID_BLUE + // over VPN. + // Additionally, the VPN sends 6000 bytes (600 packets) of its own traffic into the tun + // interface (passing that traffic to the VPN endpoint), and receives 5000 bytes (500 + // packets) from it. Including overhead that is 6600/5500 bytes. + // VPN sent 8250 bytes (750 packets), and received 8800 (800 packets) over WiFi. + // Of 8250 bytes sent over WiFi, expect 1000 bytes attributed to UID_RED, 500 bytes + // attributed to UID_BLUE, and 6750 bytes attributed to UID_VPN. + // Of 8800 bytes received over WiFi, expect 2000 bytes attributed to UID_RED, 1000 bytes + // attributed to UID_BLUE, and 5800 bytes attributed to UID_VPN. + final NetworkStats tunStats = + parseDetailedStats(R.raw.xt_qtaguid_vpn_one_underlying_own_traffic); + + assertValues(tunStats, TEST_IFACE, UID_RED, 2000L, 200L, 1000L, 100L); + assertValues(tunStats, TEST_IFACE, UID_BLUE, 1000L, 100L, 500L, 50L); + assertValues(tunStats, TEST_IFACE, UID_VPN, 5800L, 500L, 6750L, 600L); + } + + @Test + public void testVpnWithOneUnderlyingIface_withCompression() throws Exception { + // WiFi network is connected and VPN is using WiFi (which has TEST_IFACE). + final UnderlyingNetworkInfo[] underlyingNetworkInfos = + new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE})}; + mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); + + // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption + // overhead per packet): + // 1000 bytes (100 packets) were sent/received by UID_RED over VPN. + // 3000 bytes (300 packets) were sent/received by UID_BLUE over VPN. + // VPN sent/received 1000 bytes (100 packets) over WiFi. + // Of 1000 bytes over WiFi, expect 250 bytes attributed UID_RED and 750 bytes to UID_BLUE, + // with nothing attributed to UID_VPN for both rx/tx traffic. + final NetworkStats tunStats = + parseDetailedStats(R.raw.xt_qtaguid_vpn_one_underlying_compression); + + assertValues(tunStats, TEST_IFACE, UID_RED, 250L, 25L, 250L, 25L); + assertValues(tunStats, TEST_IFACE, UID_BLUE, 750L, 75L, 750L, 75L); + assertValues(tunStats, TEST_IFACE, UID_VPN, 0L, 0L, 0L, 0L); + } + + @Test + public void testVpnWithTwoUnderlyingIfaces_packetDuplication() throws Exception { + // WiFi and Cell networks are connected and VPN is using WiFi (which has TEST_IFACE) and + // Cell (which has TEST_IFACE2) and has declared both of them in its underlying network set. + // Additionally, VPN is duplicating traffic across both WiFi and Cell. + final UnderlyingNetworkInfo[] underlyingNetworkInfos = + new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE, TEST_IFACE2})}; + mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); + + // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption + // overhead per packet): + // 1000 bytes (100 packets) were sent/received by UID_RED and UID_BLUE over VPN. + // VPN sent/received 4400 bytes (400 packets) over both WiFi and Cell (8800 bytes in total). + // Of 8800 bytes over WiFi/Cell, expect: + // - 500 bytes rx/tx each over WiFi/Cell attributed to both UID_RED and UID_BLUE. + // - 1200 bytes rx/tx each over WiFi/Cell for VPN_UID. + final NetworkStats tunStats = + parseDetailedStats(R.raw.xt_qtaguid_vpn_two_underlying_duplication); + + assertValues(tunStats, TEST_IFACE, UID_RED, 500L, 50L, 500L, 50L); + assertValues(tunStats, TEST_IFACE, UID_BLUE, 500L, 50L, 500L, 50L); + assertValues(tunStats, TEST_IFACE, UID_VPN, 1200L, 100L, 1200L, 100L); + assertValues(tunStats, TEST_IFACE2, UID_RED, 500L, 50L, 500L, 50L); + assertValues(tunStats, TEST_IFACE2, UID_BLUE, 500L, 50L, 500L, 50L); + assertValues(tunStats, TEST_IFACE2, UID_VPN, 1200L, 100L, 1200L, 100L); + } + + @Test + public void testConcurrentVpns() throws Exception { + // Assume two VPNs are connected on two different network interfaces. VPN1 is using + // TEST_IFACE and VPN2 is using TEST_IFACE2. + final UnderlyingNetworkInfo[] underlyingNetworkInfos = new UnderlyingNetworkInfo[] { + createVpnInfo(TUN_IFACE, new String[] {TEST_IFACE}), + createVpnInfo(TUN_IFACE2, new String[] {TEST_IFACE2})}; + mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); + + // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption + // overhead per packet): + // 1000 bytes (100 packets) were sent, and 2000 bytes (200 packets) were received by UID_RED + // over VPN1. + // 700 bytes (70 packets) were sent, and 3000 bytes (300 packets) were received by UID_RED + // over VPN2. + // 500 bytes (50 packets) were sent, and 1000 bytes (100 packets) were received by UID_BLUE + // over VPN1. + // 250 bytes (25 packets) were sent, and 500 bytes (50 packets) were received by UID_BLUE + // over VPN2. + // VPN1 sent 1650 bytes (150 packets), and received 3300 (300 packets) over TEST_IFACE. + // Of 1650 bytes sent over WiFi, expect 1000 bytes attributed to UID_RED, 500 bytes + // attributed to UID_BLUE, and 150 bytes attributed to UID_VPN. + // Of 3300 bytes received over WiFi, expect 2000 bytes attributed to UID_RED, 1000 bytes + // attributed to UID_BLUE, and 300 bytes attributed to UID_VPN. + // VPN2 sent 1045 bytes (95 packets), and received 3850 (350 packets) over TEST_IFACE2. + // Of 1045 bytes sent over Cell, expect 700 bytes attributed to UID_RED, 250 bytes + // attributed to UID_BLUE, and 95 bytes attributed to UID_VPN. + // Of 3850 bytes received over Cell, expect 3000 bytes attributed to UID_RED, 500 bytes + // attributed to UID_BLUE, and 350 bytes attributed to UID_VPN. + final NetworkStats tunStats = + parseDetailedStats(R.raw.xt_qtaguid_vpn_one_underlying_two_vpn); + + assertValues(tunStats, TEST_IFACE, UID_RED, 2000L, 200L, 1000L, 100L); + assertValues(tunStats, TEST_IFACE, UID_BLUE, 1000L, 100L, 500L, 50L); + assertValues(tunStats, TEST_IFACE2, UID_RED, 3000L, 300L, 700L, 70L); + assertValues(tunStats, TEST_IFACE2, UID_BLUE, 500L, 50L, 250L, 25L); + assertValues(tunStats, TEST_IFACE, UID_VPN, 300L, 0L, 150L, 0L); + assertValues(tunStats, TEST_IFACE2, UID_VPN, 350L, 0L, 95L, 0L); + } + + @Test + public void testVpnWithTwoUnderlyingIfaces_splitTraffic() throws Exception { + // WiFi and Cell networks are connected and VPN is using WiFi (which has TEST_IFACE) and + // Cell (which has TEST_IFACE2) and has declared both of them in its underlying network set. + // Additionally, VPN is arbitrarily splitting traffic across WiFi and Cell. + final UnderlyingNetworkInfo[] underlyingNetworkInfos = + new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE, TEST_IFACE2})}; + mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); + + // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption + // overhead per packet): + // 1000 bytes (100 packets) were sent, and 500 bytes (50 packets) received by UID_RED over + // VPN. + // VPN sent 660 bytes (60 packets) over WiFi and 440 bytes (40 packets) over Cell. + // And, it received 330 bytes (30 packets) over WiFi and 220 bytes (20 packets) over Cell. + // For UID_RED, expect 600 bytes attributed over WiFi and 400 bytes over Cell for sent (tx) + // traffic. For received (rx) traffic, expect 300 bytes over WiFi and 200 bytes over Cell. + // + // For UID_VPN, expect 60 bytes attributed over WiFi and 40 bytes over Cell for tx traffic. + // And, 30 bytes over WiFi and 20 bytes over Cell for rx traffic. + final NetworkStats tunStats = parseDetailedStats(R.raw.xt_qtaguid_vpn_two_underlying_split); + + assertValues(tunStats, TEST_IFACE, UID_RED, 300L, 30L, 600L, 60L); + assertValues(tunStats, TEST_IFACE, UID_VPN, 30L, 0L, 60L, 0L); + assertValues(tunStats, TEST_IFACE2, UID_RED, 200L, 20L, 400L, 40L); + assertValues(tunStats, TEST_IFACE2, UID_VPN, 20L, 0L, 40L, 0L); + } + + @Test + public void testVpnWithTwoUnderlyingIfaces_splitTrafficWithCompression() throws Exception { + // WiFi and Cell networks are connected and VPN is using WiFi (which has TEST_IFACE) and + // Cell (which has TEST_IFACE2) and has declared both of them in its underlying network set. + // Additionally, VPN is arbitrarily splitting compressed traffic across WiFi and Cell. + final UnderlyingNetworkInfo[] underlyingNetworkInfos = + new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE, TEST_IFACE2})}; + mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); + + // create some traffic (assume 10 bytes of MTU for VPN interface: + // 1000 bytes (100 packets) were sent/received by UID_RED over VPN. + // VPN sent/received 600 bytes (60 packets) over WiFi and 200 bytes (20 packets) over Cell. + // For UID_RED, expect 600 bytes attributed over WiFi and 200 bytes over Cell for both + // rx/tx. + // UID_VPN gets nothing attributed to it (avoiding negative stats). + final NetworkStats tunStats = + parseDetailedStats(R.raw.xt_qtaguid_vpn_two_underlying_split_compression); + + assertValues(tunStats, TEST_IFACE, UID_RED, 600L, 60L, 600L, 60L); + assertValues(tunStats, TEST_IFACE, UID_VPN, 0L, 0L, 0L, 0L); + assertValues(tunStats, TEST_IFACE2, UID_RED, 200L, 20L, 200L, 20L); + assertValues(tunStats, TEST_IFACE2, UID_VPN, 0L, 0L, 0L, 0L); + } + + @Test + public void testVpnWithIncorrectUnderlyingIface() throws Exception { + // WiFi and Cell networks are connected and VPN is using Cell (which has TEST_IFACE2), + // but has declared only WiFi (TEST_IFACE) in its underlying network set. + final UnderlyingNetworkInfo[] underlyingNetworkInfos = + new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE})}; + mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); + + // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption + // overhead per packet): + // 1000 bytes (100 packets) were sent/received by UID_RED over VPN. + // VPN sent/received 1100 bytes (100 packets) over Cell. + // Of 1100 bytes over Cell, expect all of it attributed to UID_VPN for both rx/tx traffic. + final NetworkStats tunStats = parseDetailedStats(R.raw.xt_qtaguid_vpn_incorrect_iface); + + assertValues(tunStats, TEST_IFACE, UID_RED, 0L, 0L, 0L, 0L); + assertValues(tunStats, TEST_IFACE, UID_VPN, 0L, 0L, 0L, 0L); + assertValues(tunStats, TEST_IFACE2, UID_RED, 0L, 0L, 0L, 0L); + assertValues(tunStats, TEST_IFACE2, UID_VPN, 1100L, 100L, 1100L, 100L); + } + + @Test + public void testKernelTags() throws Exception { + assertEquals(0, kernelToTag("0x0000000000000000")); + assertEquals(0x32, kernelToTag("0x0000003200000000")); + assertEquals(2147483647, kernelToTag("0x7fffffff00000000")); + assertEquals(0, kernelToTag("0x0000000000000000")); + assertEquals(2147483136, kernelToTag("0x7FFFFE0000000000")); + + assertEquals(0, kernelToTag("0x0")); + assertEquals(0, kernelToTag("0xf00d")); + assertEquals(1, kernelToTag("0x100000000")); + assertEquals(14438007, kernelToTag("0xdc4e7700000000")); + assertEquals(TrafficStats.TAG_SYSTEM_DOWNLOAD, kernelToTag("0xffffff0100000000")); + } + + @Test + public void testNetworkStatsWithSet() throws Exception { + final NetworkStats stats = parseDetailedStats(R.raw.xt_qtaguid_typical); + assertEquals(70, stats.size()); + assertStatsEntry(stats, "rmnet1", 10021, SET_DEFAULT, 0x30100000, 219110L, 578L, 227423L, + 676L); + assertStatsEntry(stats, "rmnet1", 10021, SET_FOREGROUND, 0x30100000, 742L, 3L, 1265L, 3L); + } + + @Test + public void testNetworkStatsSingle() throws Exception { + stageFile(R.raw.xt_qtaguid_iface_typical, file("net/xt_qtaguid/iface_stat_all")); + + final NetworkStats stats = mFactory.readNetworkStatsSummaryDev(); + assertEquals(6, stats.size()); + assertStatsEntry(stats, "rmnet0", UID_ALL, SET_ALL, TAG_NONE, 2112L, 24L, 700L, 10L); + assertStatsEntry(stats, "test1", UID_ALL, SET_ALL, TAG_NONE, 6L, 8L, 10L, 12L); + assertStatsEntry(stats, "test2", UID_ALL, SET_ALL, TAG_NONE, 1L, 2L, 3L, 4L); + } + + @Test + public void testNetworkStatsXt() throws Exception { + stageFile(R.raw.xt_qtaguid_iface_fmt_typical, file("net/xt_qtaguid/iface_stat_fmt")); + + final NetworkStats stats = mFactory.readNetworkStatsSummaryXt(); + assertEquals(3, stats.size()); + assertStatsEntry(stats, "rmnet0", UID_ALL, SET_ALL, TAG_NONE, 6824L, 16L, 5692L, 10L); + assertStatsEntry(stats, "rmnet1", UID_ALL, SET_ALL, TAG_NONE, 11153922L, 8051L, 190226L, + 2468L); + assertStatsEntry(stats, "rmnet2", UID_ALL, SET_ALL, TAG_NONE, 4968L, 35L, 3081L, 39L); + } + + @Test + public void testDoubleClatAccountingSimple() throws Exception { + mFactory.noteStackedIface("v4-wlan0", "wlan0"); + + // xt_qtaguid_with_clat_simple is a synthetic file that simulates + // - 213 received 464xlat packets of size 200 bytes + // - 41 sent 464xlat packets of size 100 bytes + // - no other traffic on base interface for root uid. + NetworkStats stats = parseDetailedStats(R.raw.xt_qtaguid_with_clat_simple); + assertEquals(3, stats.size()); + + assertStatsEntry(stats, "v4-wlan0", 10060, SET_DEFAULT, 0x0, 46860L, 4920L); + assertStatsEntry(stats, "wlan0", 0, SET_DEFAULT, 0x0, 0L, 0L); + } + + @Test + public void testDoubleClatAccounting() throws Exception { + mFactory.noteStackedIface("v4-wlan0", "wlan0"); + + NetworkStats stats = parseDetailedStats(R.raw.xt_qtaguid_with_clat); + assertEquals(42, stats.size()); + + assertStatsEntry(stats, "v4-wlan0", 0, SET_DEFAULT, 0x0, 356L, 276L); + assertStatsEntry(stats, "v4-wlan0", 1000, SET_DEFAULT, 0x0, 30812L, 2310L); + assertStatsEntry(stats, "v4-wlan0", 10102, SET_DEFAULT, 0x0, 10022L, 3330L); + assertStatsEntry(stats, "v4-wlan0", 10060, SET_DEFAULT, 0x0, 9532772L, 254112L); + assertStatsEntry(stats, "wlan0", 0, SET_DEFAULT, 0x0, 0L, 0L); + assertStatsEntry(stats, "wlan0", 1000, SET_DEFAULT, 0x0, 6126L, 2013L); + assertStatsEntry(stats, "wlan0", 10013, SET_DEFAULT, 0x0, 0L, 144L); + assertStatsEntry(stats, "wlan0", 10018, SET_DEFAULT, 0x0, 5980263L, 167667L); + assertStatsEntry(stats, "wlan0", 10060, SET_DEFAULT, 0x0, 134356L, 8705L); + assertStatsEntry(stats, "wlan0", 10079, SET_DEFAULT, 0x0, 10926L, 1507L); + assertStatsEntry(stats, "wlan0", 10102, SET_DEFAULT, 0x0, 25038L, 8245L); + assertStatsEntry(stats, "wlan0", 10103, SET_DEFAULT, 0x0, 0L, 192L); + assertStatsEntry(stats, "dummy0", 0, SET_DEFAULT, 0x0, 0L, 168L); + assertStatsEntry(stats, "lo", 0, SET_DEFAULT, 0x0, 1288L, 1288L); + + assertNoStatsEntry(stats, "wlan0", 1029, SET_DEFAULT, 0x0); + } + + @Test + public void testDoubleClatAccounting100MBDownload() throws Exception { + // Downloading 100mb from an ipv4 only destination in a foreground activity + + long appRxBytesBefore = 328684029L; + long appRxBytesAfter = 439237478L; + assertEquals("App traffic should be ~100MB", 110553449, appRxBytesAfter - appRxBytesBefore); + + long rootRxBytes = 330187296L; + + mFactory.noteStackedIface("v4-wlan0", "wlan0"); + NetworkStats stats; + + // Stats snapshot before the download + stats = parseDetailedStats(R.raw.xt_qtaguid_with_clat_100mb_download_before); + assertStatsEntry(stats, "v4-wlan0", 10106, SET_FOREGROUND, 0x0, appRxBytesBefore, 5199872L); + assertStatsEntry(stats, "wlan0", 0, SET_DEFAULT, 0x0, rootRxBytes, 0L); + + // Stats snapshot after the download + stats = parseDetailedStats(R.raw.xt_qtaguid_with_clat_100mb_download_after); + assertStatsEntry(stats, "v4-wlan0", 10106, SET_FOREGROUND, 0x0, appRxBytesAfter, 7867488L); + assertStatsEntry(stats, "wlan0", 0, SET_DEFAULT, 0x0, rootRxBytes, 0L); + } + + /** + * Copy a {@link Resources#openRawResource(int)} into {@link File} for + * testing purposes. + */ + private void stageFile(int rawId, File file) throws Exception { + new File(file.getParent()).mkdirs(); + InputStream in = null; + OutputStream out = null; + try { + in = InstrumentationRegistry.getContext().getResources().openRawResource(rawId); + out = new FileOutputStream(file); + Streams.copy(in, out); + } finally { + IoUtils.closeQuietly(in); + IoUtils.closeQuietly(out); + } + } + + private void stageLong(long value, File file) throws Exception { + new File(file.getParent()).mkdirs(); + FileWriter out = null; + try { + out = new FileWriter(file); + out.write(Long.toString(value)); + } finally { + IoUtils.closeQuietly(out); + } + } + + private File file(String path) throws Exception { + return new File(mTestProc, path); + } + + private NetworkStats parseDetailedStats(int resourceId) throws Exception { + stageFile(resourceId, file("net/xt_qtaguid/stats")); + return mFactory.readNetworkStatsDetail(); + } + + private static void assertStatsEntry(NetworkStats stats, String iface, int uid, int set, + int tag, long rxBytes, long txBytes) { + final int i = stats.findIndex(iface, uid, set, tag, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO); + if (i < 0) { + fail(String.format("no NetworkStats for (iface: %s, uid: %d, set: %d, tag: %d)", + iface, uid, set, tag)); + } + final NetworkStats.Entry entry = stats.getValues(i, null); + assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes); + assertEquals("unexpected txBytes", txBytes, entry.txBytes); + } + + private static void assertNoStatsEntry(NetworkStats stats, String iface, int uid, int set, + int tag) { + final int i = stats.findIndex(iface, uid, set, tag, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO); + if (i >= 0) { + fail("unexpected NetworkStats entry at " + i); + } + } + + private static void assertStatsEntry(NetworkStats stats, String iface, int uid, int set, + int tag, long rxBytes, long rxPackets, long txBytes, long txPackets) { + assertStatsEntry(stats, iface, uid, set, tag, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, + rxBytes, rxPackets, txBytes, txPackets); + } + + private static void assertStatsEntry(NetworkStats stats, String iface, int uid, int set, + int tag, int metered, int roaming, int defaultNetwork, long rxBytes, long rxPackets, + long txBytes, long txPackets) { + final int i = stats.findIndex(iface, uid, set, tag, metered, roaming, defaultNetwork); + + if (i < 0) { + fail(String.format("no NetworkStats for (iface: %s, uid: %d, set: %d, tag: %d, metered:" + + " %d, roaming: %d, defaultNetwork: %d)", + iface, uid, set, tag, metered, roaming, defaultNetwork)); + } + final NetworkStats.Entry entry = stats.getValues(i, null); + assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes); + assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets); + assertEquals("unexpected txBytes", txBytes, entry.txBytes); + assertEquals("unexpected txPackets", txPackets, entry.txPackets); + } +} diff --git a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java new file mode 100644 index 0000000000..9fa1c50423 --- /dev/null +++ b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java @@ -0,0 +1,447 @@ +/* + * Copyright (C) 2016 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.net; + +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.NetworkIdentity.OEM_NONE; +import static android.net.NetworkStats.DEFAULT_NETWORK_NO; +import static android.net.NetworkStats.DEFAULT_NETWORK_YES; +import static android.net.NetworkStats.METERED_NO; +import static android.net.NetworkStats.ROAMING_NO; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkTemplate.buildTemplateMobileAll; +import static android.net.NetworkTemplate.buildTemplateWifiWildcard; +import static android.net.TrafficStats.MB_IN_BYTES; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; + +import android.app.usage.NetworkStatsManager; +import android.net.DataUsageRequest; +import android.net.NetworkIdentity; +import android.net.NetworkStats; +import android.net.NetworkTemplate; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Messenger; +import android.os.Process; +import android.os.UserHandle; +import android.telephony.TelephonyManager; +import android.util.ArrayMap; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.net.NetworkStatsServiceTest.LatchedHandler; +import com.android.testutils.HandlerUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.Objects; + +/** + * Tests for {@link NetworkStatsObservers}. + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkStatsObserversTest { + private static final String TEST_IFACE = "test0"; + private static final String TEST_IFACE2 = "test1"; + private static final long TEST_START = 1194220800000L; + + private static final String IMSI_1 = "310004"; + private static final String IMSI_2 = "310260"; + private static final String TEST_SSID = "AndroidAP"; + + private static NetworkTemplate sTemplateWifi = buildTemplateWifiWildcard(); + private static NetworkTemplate sTemplateImsi1 = buildTemplateMobileAll(IMSI_1); + private static NetworkTemplate sTemplateImsi2 = buildTemplateMobileAll(IMSI_2); + + private static final int UID_RED = UserHandle.PER_USER_RANGE + 1; + private static final int UID_BLUE = UserHandle.PER_USER_RANGE + 2; + private static final int UID_GREEN = UserHandle.PER_USER_RANGE + 3; + private static final int UID_ANOTHER_USER = 2 * UserHandle.PER_USER_RANGE + 4; + + private static final long WAIT_TIMEOUT_MS = 500; + private static final long THRESHOLD_BYTES = 2 * MB_IN_BYTES; + private static final long BASE_BYTES = 7 * MB_IN_BYTES; + private static final int INVALID_TYPE = -1; + + private long mElapsedRealtime; + + private HandlerThread mObserverHandlerThread; + private Handler mObserverNoopHandler; + + private LatchedHandler mHandler; + + private NetworkStatsObservers mStatsObservers; + private Messenger mMessenger; + private ArrayMap mActiveIfaces; + private ArrayMap mActiveUidIfaces; + + @Mock private IBinder mockBinder; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mObserverHandlerThread = new HandlerThread("HandlerThread"); + mObserverHandlerThread.start(); + final Looper observerLooper = mObserverHandlerThread.getLooper(); + mStatsObservers = new NetworkStatsObservers() { + @Override + protected Looper getHandlerLooperLocked() { + return observerLooper; + } + }; + + mHandler = new LatchedHandler(Looper.getMainLooper(), new ConditionVariable()); + mMessenger = new Messenger(mHandler); + + mActiveIfaces = new ArrayMap<>(); + mActiveUidIfaces = new ArrayMap<>(); + } + + @Test + public void testRegister_thresholdTooLow_setsDefaultThreshold() throws Exception { + long thresholdTooLowBytes = 1L; + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, thresholdTooLowBytes); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateWifi, request.template)); + assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); + } + + @Test + public void testRegister_highThreshold_accepted() throws Exception { + long highThresholdBytes = 2 * THRESHOLD_BYTES; + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, highThresholdBytes); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateWifi, request.template)); + assertEquals(highThresholdBytes, request.thresholdInBytes); + } + + @Test + public void testRegister_twoRequests_twoIds() throws Exception { + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, THRESHOLD_BYTES); + + DataUsageRequest request1 = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); + assertTrue(request1.requestId > 0); + assertTrue(Objects.equals(sTemplateWifi, request1.template)); + assertEquals(THRESHOLD_BYTES, request1.thresholdInBytes); + + DataUsageRequest request2 = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); + assertTrue(request2.requestId > request1.requestId); + assertTrue(Objects.equals(sTemplateWifi, request2.template)); + assertEquals(THRESHOLD_BYTES, request2.thresholdInBytes); + } + + @Test + public void testUnregister_unknownRequest_noop() throws Exception { + DataUsageRequest unknownRequest = new DataUsageRequest( + 123456 /* id */, sTemplateWifi, THRESHOLD_BYTES); + + mStatsObservers.unregister(unknownRequest, UID_RED); + } + + @Test + public void testUnregister_knownRequest_releasesCaller() throws Exception { + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateImsi1, request.template)); + assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); + Mockito.verify(mockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt()); + + mStatsObservers.unregister(request, Process.SYSTEM_UID); + waitForObserverToIdle(); + + Mockito.verify(mockBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt()); + } + + @Test + public void testUnregister_knownRequest_invalidUid_doesNotUnregister() throws Exception { + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + UID_RED, NetworkStatsAccess.Level.DEVICE); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateImsi1, request.template)); + assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); + Mockito.verify(mockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt()); + + mStatsObservers.unregister(request, UID_BLUE); + waitForObserverToIdle(); + + Mockito.verifyZeroInteractions(mockBinder); + } + + private NetworkIdentitySet makeTestIdentSet() { + NetworkIdentitySet identSet = new NetworkIdentitySet(); + identSet.add(new NetworkIdentity( + TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_UNKNOWN, + IMSI_1, null /* networkId */, false /* roaming */, true /* metered */, + true /* defaultNetwork */, OEM_NONE)); + return identSet; + } + + @Test + public void testUpdateStats_initialSample_doesNotNotify() throws Exception { + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateImsi1, request.template)); + assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); + + NetworkIdentitySet identSet = makeTestIdentSet(); + mActiveIfaces.put(TEST_IFACE, identSet); + + // Baseline + NetworkStats xtSnapshot = new NetworkStats(TEST_START, 1 /* initialSize */) + .insertEntry(TEST_IFACE, BASE_BYTES, 8L, BASE_BYTES, 16L); + NetworkStats uidSnapshot = null; + + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + waitForObserverToIdle(); + } + + @Test + public void testUpdateStats_belowThreshold_doesNotNotify() throws Exception { + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateImsi1, request.template)); + assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); + + NetworkIdentitySet identSet = makeTestIdentSet(); + mActiveIfaces.put(TEST_IFACE, identSet); + + // Baseline + NetworkStats xtSnapshot = new NetworkStats(TEST_START, 1 /* initialSize */) + .insertEntry(TEST_IFACE, BASE_BYTES, 8L, BASE_BYTES, 16L); + NetworkStats uidSnapshot = null; + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + + // Delta + xtSnapshot = new NetworkStats(TEST_START, 1 /* initialSize */) + .insertEntry(TEST_IFACE, BASE_BYTES + 1024L, 10L, BASE_BYTES + 2048L, 20L); + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + waitForObserverToIdle(); + } + + + @Test + public void testUpdateStats_deviceAccess_notifies() throws Exception { + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateImsi1, request.template)); + assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); + + NetworkIdentitySet identSet = makeTestIdentSet(); + mActiveIfaces.put(TEST_IFACE, identSet); + + // Baseline + NetworkStats xtSnapshot = new NetworkStats(TEST_START, 1 /* initialSize */) + .insertEntry(TEST_IFACE, BASE_BYTES, 8L, BASE_BYTES, 16L); + NetworkStats uidSnapshot = null; + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + + // Delta + xtSnapshot = new NetworkStats(TEST_START + MINUTE_IN_MILLIS, 1 /* initialSize */) + .insertEntry(TEST_IFACE, BASE_BYTES + THRESHOLD_BYTES, 12L, + BASE_BYTES + THRESHOLD_BYTES, 22L); + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + waitForObserverToIdle(); + assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType); + } + + @Test + public void testUpdateStats_defaultAccess_notifiesSameUid() throws Exception { + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + UID_RED, NetworkStatsAccess.Level.DEFAULT); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateImsi1, request.template)); + assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); + + NetworkIdentitySet identSet = makeTestIdentSet(); + mActiveUidIfaces.put(TEST_IFACE, identSet); + + // Baseline + NetworkStats xtSnapshot = null; + NetworkStats uidSnapshot = new NetworkStats(TEST_START, 2 /* initialSize */) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, BASE_BYTES, 2L, BASE_BYTES, 2L, 0L); + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + + // Delta + uidSnapshot = new NetworkStats(TEST_START + 2 * MINUTE_IN_MILLIS, 2 /* initialSize */) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, BASE_BYTES + THRESHOLD_BYTES, 2L, + BASE_BYTES + THRESHOLD_BYTES, 2L, 0L); + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + waitForObserverToIdle(); + assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType); + } + + @Test + public void testUpdateStats_defaultAccess_usageOtherUid_doesNotNotify() throws Exception { + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + UID_BLUE, NetworkStatsAccess.Level.DEFAULT); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateImsi1, request.template)); + assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); + + NetworkIdentitySet identSet = makeTestIdentSet(); + mActiveUidIfaces.put(TEST_IFACE, identSet); + + // Baseline + NetworkStats xtSnapshot = null; + NetworkStats uidSnapshot = new NetworkStats(TEST_START, 2 /* initialSize */) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, BASE_BYTES, 2L, BASE_BYTES, 2L, 0L); + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + + // Delta + uidSnapshot = new NetworkStats(TEST_START + 2 * MINUTE_IN_MILLIS, 2 /* initialSize */) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, BASE_BYTES + THRESHOLD_BYTES, 2L, + BASE_BYTES + THRESHOLD_BYTES, 2L, 0L); + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + waitForObserverToIdle(); + } + + @Test + public void testUpdateStats_userAccess_usageSameUser_notifies() throws Exception { + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + UID_BLUE, NetworkStatsAccess.Level.USER); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateImsi1, request.template)); + assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); + + NetworkIdentitySet identSet = makeTestIdentSet(); + mActiveUidIfaces.put(TEST_IFACE, identSet); + + // Baseline + NetworkStats xtSnapshot = null; + NetworkStats uidSnapshot = new NetworkStats(TEST_START, 2 /* initialSize */) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, BASE_BYTES, 2L, BASE_BYTES, 2L, 0L); + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + + // Delta + uidSnapshot = new NetworkStats(TEST_START + 2 * MINUTE_IN_MILLIS, 2 /* initialSize */) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, BASE_BYTES + THRESHOLD_BYTES, 2L, + BASE_BYTES + THRESHOLD_BYTES, 2L, 0L); + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + waitForObserverToIdle(); + assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType); + } + + @Test + public void testUpdateStats_userAccess_usageAnotherUser_doesNotNotify() throws Exception { + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + UID_RED, NetworkStatsAccess.Level.USER); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateImsi1, request.template)); + assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); + + NetworkIdentitySet identSet = makeTestIdentSet(); + mActiveUidIfaces.put(TEST_IFACE, identSet); + + // Baseline + NetworkStats xtSnapshot = null; + NetworkStats uidSnapshot = new NetworkStats(TEST_START, 2 /* initialSize */) + .insertEntry(TEST_IFACE, UID_ANOTHER_USER, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_YES, BASE_BYTES, 2L, BASE_BYTES, 2L, 0L); + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + + // Delta + uidSnapshot = new NetworkStats(TEST_START + 2 * MINUTE_IN_MILLIS, 2 /* initialSize */) + .insertEntry(TEST_IFACE, UID_ANOTHER_USER, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, BASE_BYTES + THRESHOLD_BYTES, 2L, + BASE_BYTES + THRESHOLD_BYTES, 2L, 0L); + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + waitForObserverToIdle(); + } + + private void waitForObserverToIdle() { + HandlerUtils.waitForIdle(mObserverHandlerThread, WAIT_TIMEOUT_MS); + HandlerUtils.waitForIdle(mHandler, WAIT_TIMEOUT_MS); + } +} diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java new file mode 100644 index 0000000000..fd374bc9e6 --- /dev/null +++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java @@ -0,0 +1,1767 @@ +/* + * Copyright (C) 2011 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.net; + +import static android.content.Intent.ACTION_UID_REMOVED; +import static android.content.Intent.EXTRA_UID; +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.ConnectivityManager.TYPE_WIFI; +import static android.net.NetworkIdentity.OEM_PAID; +import static android.net.NetworkIdentity.OEM_PRIVATE; +import static android.net.NetworkStats.DEFAULT_NETWORK_ALL; +import static android.net.NetworkStats.DEFAULT_NETWORK_NO; +import static android.net.NetworkStats.DEFAULT_NETWORK_YES; +import static android.net.NetworkStats.IFACE_ALL; +import static android.net.NetworkStats.INTERFACES_ALL; +import static android.net.NetworkStats.METERED_ALL; +import static android.net.NetworkStats.METERED_NO; +import static android.net.NetworkStats.METERED_YES; +import static android.net.NetworkStats.ROAMING_ALL; +import static android.net.NetworkStats.ROAMING_NO; +import static android.net.NetworkStats.ROAMING_YES; +import static android.net.NetworkStats.SET_ALL; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.SET_FOREGROUND; +import static android.net.NetworkStats.STATS_PER_UID; +import static android.net.NetworkStats.TAG_ALL; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkStats.UID_ALL; +import static android.net.NetworkStatsHistory.FIELD_ALL; +import static android.net.NetworkTemplate.MATCH_MOBILE_WILDCARD; +import static android.net.NetworkTemplate.NETWORK_TYPE_ALL; +import static android.net.NetworkTemplate.OEM_MANAGED_NO; +import static android.net.NetworkTemplate.OEM_MANAGED_YES; +import static android.net.NetworkTemplate.SUBSCRIBER_ID_MATCH_RULE_EXACT; +import static android.net.NetworkTemplate.buildTemplateMobileAll; +import static android.net.NetworkTemplate.buildTemplateMobileWithRatType; +import static android.net.NetworkTemplate.buildTemplateWifi; +import static android.net.NetworkTemplate.buildTemplateWifiWildcard; +import static android.net.TrafficStats.MB_IN_BYTES; +import static android.net.TrafficStats.UID_REMOVED; +import static android.net.TrafficStats.UID_TETHERING; +import static android.text.format.DateUtils.DAY_IN_MILLIS; +import static android.text.format.DateUtils.HOUR_IN_MILLIS; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; +import static android.text.format.DateUtils.WEEK_IN_MILLIS; + +import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.annotation.NonNull; +import android.app.AlarmManager; +import android.app.usage.NetworkStatsManager; +import android.content.Context; +import android.content.Intent; +import android.database.ContentObserver; +import android.net.DataUsageRequest; +import android.net.INetworkManagementEventObserver; +import android.net.INetworkStatsSession; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkStateSnapshot; +import android.net.NetworkStats; +import android.net.NetworkStatsHistory; +import android.net.NetworkTemplate; +import android.net.TelephonyNetworkSpecifier; +import android.net.UnderlyingNetworkInfo; +import android.net.netstats.provider.INetworkStatsProviderCallback; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.INetworkManagementService; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.os.PowerManager; +import android.os.SimpleClock; +import android.provider.Settings; +import android.telephony.TelephonyManager; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.test.BroadcastInterceptingContext; +import com.android.server.net.NetworkStatsService.NetworkStatsSettings; +import com.android.server.net.NetworkStatsService.NetworkStatsSettings.Config; +import com.android.testutils.HandlerUtils; +import com.android.testutils.TestableNetworkStatsProviderBinder; + +import libcore.io.IoUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.File; +import java.time.Clock; +import java.time.ZoneOffset; +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * Tests for {@link NetworkStatsService}. + * + * TODO: This test used to be really brittle because it used Easymock - it uses Mockito now, but + * still uses the Easymock structure, which could be simplified. + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkStatsServiceTest extends NetworkStatsBaseTest { + private static final String TAG = "NetworkStatsServiceTest"; + + private static final long TEST_START = 1194220800000L; + + private static final String IMSI_1 = "310004"; + private static final String IMSI_2 = "310260"; + private static final String TEST_SSID = "AndroidAP"; + + private static NetworkTemplate sTemplateWifi = buildTemplateWifi(TEST_SSID); + private static NetworkTemplate sTemplateImsi1 = buildTemplateMobileAll(IMSI_1); + private static NetworkTemplate sTemplateImsi2 = buildTemplateMobileAll(IMSI_2); + + private static final Network WIFI_NETWORK = new Network(100); + private static final Network MOBILE_NETWORK = new Network(101); + private static final Network VPN_NETWORK = new Network(102); + + private static final Network[] NETWORKS_WIFI = new Network[]{ WIFI_NETWORK }; + private static final Network[] NETWORKS_MOBILE = new Network[]{ MOBILE_NETWORK }; + + private static final long WAIT_TIMEOUT = 2 * 1000; // 2 secs + private static final int INVALID_TYPE = -1; + + private long mElapsedRealtime; + + private File mStatsDir; + private MockContext mServiceContext; + private @Mock TelephonyManager mTelephonyManager; + private @Mock INetworkManagementService mNetManager; + private @Mock NetworkStatsFactory mStatsFactory; + private @Mock NetworkStatsSettings mSettings; + private @Mock IBinder mBinder; + private @Mock AlarmManager mAlarmManager; + @Mock + private NetworkStatsSubscriptionsMonitor mNetworkStatsSubscriptionsMonitor; + private HandlerThread mHandlerThread; + + private NetworkStatsService mService; + private INetworkStatsSession mSession; + private INetworkManagementEventObserver mNetworkObserver; + private ContentObserver mContentObserver; + private Handler mHandler; + + private class MockContext extends BroadcastInterceptingContext { + private final Context mBaseContext; + + MockContext(Context base) { + super(base); + mBaseContext = base; + } + + @Override + public Object getSystemService(String name) { + if (Context.TELEPHONY_SERVICE.equals(name)) return mTelephonyManager; + return mBaseContext.getSystemService(name); + } + } + + private final Clock mClock = new SimpleClock(ZoneOffset.UTC) { + @Override + public long millis() { + return currentTimeMillis(); + } + }; + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + final Context context = InstrumentationRegistry.getContext(); + mServiceContext = new MockContext(context); + mStatsDir = context.getFilesDir(); + if (mStatsDir.exists()) { + IoUtils.deleteContents(mStatsDir); + } + + PowerManager powerManager = (PowerManager) mServiceContext.getSystemService( + Context.POWER_SERVICE); + PowerManager.WakeLock wakeLock = + powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + + mHandlerThread = new HandlerThread("HandlerThread"); + final NetworkStatsService.Dependencies deps = makeDependencies(); + mService = new NetworkStatsService(mServiceContext, mNetManager, mAlarmManager, wakeLock, + mClock, mSettings, mStatsFactory, new NetworkStatsObservers(), mStatsDir, + getBaseDir(mStatsDir), deps); + + mElapsedRealtime = 0L; + + expectDefaultSettings(); + expectNetworkStatsUidDetail(buildEmptyStats()); + expectSystemReady(); + mService.systemReady(); + // Verify that system ready fetches realtime stats + verify(mStatsFactory).readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL); + // Wait for posting onChange() event to handler thread and verify that when system ready, + // start monitoring data usage per RAT type because the settings value is mock as false + // by default in expectSettings(). + waitForIdle(); + verify(mNetworkStatsSubscriptionsMonitor).start(); + reset(mNetworkStatsSubscriptionsMonitor); + + doReturn(TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS).when(mTelephonyManager) + .checkCarrierPrivilegesForPackageAnyPhone(anyString()); + + mSession = mService.openSession(); + assertNotNull("openSession() failed", mSession); + + // catch INetworkManagementEventObserver during systemReady() + ArgumentCaptor networkObserver = + ArgumentCaptor.forClass(INetworkManagementEventObserver.class); + verify(mNetManager).registerObserver(networkObserver.capture()); + mNetworkObserver = networkObserver.getValue(); + } + + @NonNull + private NetworkStatsService.Dependencies makeDependencies() { + return new NetworkStatsService.Dependencies() { + @Override + public HandlerThread makeHandlerThread() { + return mHandlerThread; + } + + @Override + public NetworkStatsSubscriptionsMonitor makeSubscriptionsMonitor( + @NonNull Context context, @NonNull Looper looper, @NonNull Executor executor, + @NonNull NetworkStatsService service) { + + return mNetworkStatsSubscriptionsMonitor; + } + + @Override + public ContentObserver makeContentObserver(Handler handler, + NetworkStatsSettings settings, NetworkStatsSubscriptionsMonitor monitor) { + mHandler = handler; + return mContentObserver = super.makeContentObserver(handler, settings, monitor); + } + + }; + } + + @After + public void tearDown() throws Exception { + IoUtils.deleteContents(mStatsDir); + + mServiceContext = null; + mStatsDir = null; + + mNetManager = null; + mSettings = null; + + mSession.close(); + mService = null; + + mHandlerThread.quitSafely(); + } + + @Test + public void testNetworkStatsWifi() throws Exception { + // pretend that wifi network comes online; service should ask about full + // network state, and poll any existing interfaces before updating. + expectDefaultSettings(); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // verify service has empty history for wifi + assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0); + + // modify some number on wifi, and trigger poll event + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, 1024L, 1L, 2048L, 2L)); + expectNetworkStatsUidDetail(buildEmptyStats()); + forcePollAndWaitForIdle(); + + // verify service recorded history + assertNetworkTotal(sTemplateWifi, 1024L, 1L, 2048L, 2L, 0); + + + // and bump forward again, with counters going higher. this is + // important, since polling should correctly subtract last snapshot. + incrementCurrentTime(DAY_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, 4096L, 4L, 8192L, 8L)); + expectNetworkStatsUidDetail(buildEmptyStats()); + forcePollAndWaitForIdle(); + + // verify service recorded history + assertNetworkTotal(sTemplateWifi, 4096L, 4L, 8192L, 8L, 0); + + } + + @Test + public void testStatsRebootPersist() throws Exception { + assertStatsFilesExist(false); + + // pretend that wifi network comes online; service should ask about full + // network state, and poll any existing interfaces before updating. + expectDefaultSettings(); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // verify service has empty history for wifi + assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0); + + + // modify some number on wifi, and trigger poll event + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, 1024L, 8L, 2048L, 16L)); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 512L, 4L, 256L, 2L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xFAAD, 256L, 2L, 128L, 1L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, 512L, 4L, 256L, 2L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, 0xFAAD, 256L, 2L, 128L, 1L, 0L) + .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 0L)); + mService.setUidForeground(UID_RED, false); + mService.incrementOperationCount(UID_RED, 0xFAAD, 4); + mService.setUidForeground(UID_RED, true); + mService.incrementOperationCount(UID_RED, 0xFAAD, 6); + + forcePollAndWaitForIdle(); + + // verify service recorded history + assertNetworkTotal(sTemplateWifi, 1024L, 8L, 2048L, 16L, 0); + assertUidTotal(sTemplateWifi, UID_RED, 1024L, 8L, 512L, 4L, 10); + assertUidTotal(sTemplateWifi, UID_RED, SET_DEFAULT, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 512L, 4L, 256L, 2L, 4); + assertUidTotal(sTemplateWifi, UID_RED, SET_FOREGROUND, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 512L, 4L, 256L, 2L, 6); + assertUidTotal(sTemplateWifi, UID_BLUE, 128L, 1L, 128L, 1L, 0); + + + // graceful shutdown system, which should trigger persist of stats, and + // clear any values in memory. + expectDefaultSettings(); + mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN)); + assertStatsFilesExist(true); + + // boot through serviceReady() again + expectDefaultSettings(); + expectNetworkStatsUidDetail(buildEmptyStats()); + expectSystemReady(); + + mService.systemReady(); + + // after systemReady(), we should have historical stats loaded again + assertNetworkTotal(sTemplateWifi, 1024L, 8L, 2048L, 16L, 0); + assertUidTotal(sTemplateWifi, UID_RED, 1024L, 8L, 512L, 4L, 10); + assertUidTotal(sTemplateWifi, UID_RED, SET_DEFAULT, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 512L, 4L, 256L, 2L, 4); + assertUidTotal(sTemplateWifi, UID_RED, SET_FOREGROUND, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 512L, 4L, 256L, 2L, 6); + assertUidTotal(sTemplateWifi, UID_BLUE, 128L, 1L, 128L, 1L, 0); + + } + + // TODO: simulate reboot to test bucket resize + @Test + @Ignore + public void testStatsBucketResize() throws Exception { + NetworkStatsHistory history = null; + + assertStatsFilesExist(false); + + // pretend that wifi network comes online; service should ask about full + // network state, and poll any existing interfaces before updating. + expectSettings(0L, HOUR_IN_MILLIS, WEEK_IN_MILLIS); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // modify some number on wifi, and trigger poll event + incrementCurrentTime(2 * HOUR_IN_MILLIS); + expectSettings(0L, HOUR_IN_MILLIS, WEEK_IN_MILLIS); + expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, 512L, 4L, 512L, 4L)); + expectNetworkStatsUidDetail(buildEmptyStats()); + forcePollAndWaitForIdle(); + + // verify service recorded history + history = mSession.getHistoryForNetwork(sTemplateWifi, FIELD_ALL); + assertValues(history, Long.MIN_VALUE, Long.MAX_VALUE, 512L, 4L, 512L, 4L, 0); + assertEquals(HOUR_IN_MILLIS, history.getBucketDuration()); + assertEquals(2, history.size()); + + + // now change bucket duration setting and trigger another poll with + // exact same values, which should resize existing buckets. + expectSettings(0L, 30 * MINUTE_IN_MILLIS, WEEK_IN_MILLIS); + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + forcePollAndWaitForIdle(); + + // verify identical stats, but spread across 4 buckets now + history = mSession.getHistoryForNetwork(sTemplateWifi, FIELD_ALL); + assertValues(history, Long.MIN_VALUE, Long.MAX_VALUE, 512L, 4L, 512L, 4L, 0); + assertEquals(30 * MINUTE_IN_MILLIS, history.getBucketDuration()); + assertEquals(4, history.size()); + + } + + @Test + public void testUidStatsAcrossNetworks() throws Exception { + // pretend first mobile network comes online + expectDefaultSettings(); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildMobile3gState(IMSI_1)}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // create some traffic on first network + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, 2048L, 16L, 512L, 4L)); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1536L, 12L, 512L, 4L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L) + .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 512L, 4L, 0L, 0L, 0L)); + mService.incrementOperationCount(UID_RED, 0xF00D, 10); + + forcePollAndWaitForIdle(); + + // verify service recorded history + assertNetworkTotal(sTemplateImsi1, 2048L, 16L, 512L, 4L, 0); + assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0); + assertUidTotal(sTemplateImsi1, UID_RED, 1536L, 12L, 512L, 4L, 10); + assertUidTotal(sTemplateImsi1, UID_BLUE, 512L, 4L, 0L, 0L, 0); + + + // now switch networks; this also tests that we're okay with interfaces + // disappearing, to verify we don't count backwards. + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + states = new NetworkStateSnapshot[] {buildMobile3gState(IMSI_2)}; + expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, 2048L, 16L, 512L, 4L)); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1536L, 12L, 512L, 4L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L) + .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 512L, 4L, 0L, 0L, 0L)); + + mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + forcePollAndWaitForIdle(); + + + // create traffic on second network + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, 2176L, 17L, 1536L, 12L)); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1536L, 12L, 512L, 4L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L) + .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 640L, 5L, 1024L, 8L, 0L) + .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, 0xFAAD, 128L, 1L, 1024L, 8L, 0L)); + mService.incrementOperationCount(UID_BLUE, 0xFAAD, 10); + + forcePollAndWaitForIdle(); + + // verify original history still intact + assertNetworkTotal(sTemplateImsi1, 2048L, 16L, 512L, 4L, 0); + assertUidTotal(sTemplateImsi1, UID_RED, 1536L, 12L, 512L, 4L, 10); + assertUidTotal(sTemplateImsi1, UID_BLUE, 512L, 4L, 0L, 0L, 0); + + // and verify new history also recorded under different template, which + // verifies that we didn't cross the streams. + assertNetworkTotal(sTemplateImsi2, 128L, 1L, 1024L, 8L, 0); + assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0); + assertUidTotal(sTemplateImsi2, UID_BLUE, 128L, 1L, 1024L, 8L, 10); + + } + + @Test + public void testUidRemovedIsMoved() throws Exception { + // pretend that network comes online + expectDefaultSettings(); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // create some traffic + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, 4128L, 258L, 544L, 34L)); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xFAAD, 16L, 1L, 16L, 1L, 0L) + .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, + 4096L, 258L, 512L, 32L, 0L) + .insertEntry(TEST_IFACE, UID_GREEN, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L)); + mService.incrementOperationCount(UID_RED, 0xFAAD, 10); + + forcePollAndWaitForIdle(); + + // verify service recorded history + assertNetworkTotal(sTemplateWifi, 4128L, 258L, 544L, 34L, 0); + assertUidTotal(sTemplateWifi, UID_RED, 16L, 1L, 16L, 1L, 10); + assertUidTotal(sTemplateWifi, UID_BLUE, 4096L, 258L, 512L, 32L, 0); + assertUidTotal(sTemplateWifi, UID_GREEN, 16L, 1L, 16L, 1L, 0); + + + // now pretend two UIDs are uninstalled, which should migrate stats to + // special "removed" bucket. + expectDefaultSettings(); + expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, 4128L, 258L, 544L, 34L)); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xFAAD, 16L, 1L, 16L, 1L, 0L) + .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, + 4096L, 258L, 512L, 32L, 0L) + .insertEntry(TEST_IFACE, UID_GREEN, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L)); + final Intent intent = new Intent(ACTION_UID_REMOVED); + intent.putExtra(EXTRA_UID, UID_BLUE); + mServiceContext.sendBroadcast(intent); + intent.putExtra(EXTRA_UID, UID_RED); + mServiceContext.sendBroadcast(intent); + + // existing uid and total should remain unchanged; but removed UID + // should be gone completely. + assertNetworkTotal(sTemplateWifi, 4128L, 258L, 544L, 34L, 0); + assertUidTotal(sTemplateWifi, UID_RED, 0L, 0L, 0L, 0L, 0); + assertUidTotal(sTemplateWifi, UID_BLUE, 0L, 0L, 0L, 0L, 0); + assertUidTotal(sTemplateWifi, UID_GREEN, 16L, 1L, 16L, 1L, 0); + assertUidTotal(sTemplateWifi, UID_REMOVED, 4112L, 259L, 528L, 33L, 10); + + } + + @Test + public void testMobileStatsByRatType() throws Exception { + final NetworkTemplate template3g = + buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UMTS); + final NetworkTemplate template4g = + buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_LTE); + final NetworkTemplate template5g = + buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_NR); + final NetworkStateSnapshot[] states = + new NetworkStateSnapshot[]{buildMobile3gState(IMSI_1)}; + + // 3G network comes online. + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_UMTS); + mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Create some traffic. + incrementCurrentTime(MINUTE_IN_MILLIS); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 12L, 18L, 14L, 1L, 0L))); + forcePollAndWaitForIdle(); + + // Verify 3g templates gets stats. + assertUidTotal(sTemplateImsi1, UID_RED, 12L, 18L, 14L, 1L, 0); + assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0); + assertUidTotal(template4g, UID_RED, 0L, 0L, 0L, 0L, 0); + assertUidTotal(template5g, UID_RED, 0L, 0L, 0L, 0L, 0); + + // 4G network comes online. + incrementCurrentTime(MINUTE_IN_MILLIS); + setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_LTE); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + // Append more traffic on existing 3g stats entry. + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 16L, 22L, 17L, 2L, 0L)) + // Add entry that is new on 4g. + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, + 33L, 27L, 8L, 10L, 1L))); + forcePollAndWaitForIdle(); + + // Verify ALL_MOBILE template gets all. 3g template counters do not increase. + assertUidTotal(sTemplateImsi1, UID_RED, 49L, 49L, 25L, 12L, 1); + assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0); + // Verify 4g template counts appended stats on existing entry and newly created entry. + assertUidTotal(template4g, UID_RED, 4L + 33L, 4L + 27L, 3L + 8L, 1L + 10L, 1); + // Verify 5g template doesn't get anything since no traffic is generated on 5g. + assertUidTotal(template5g, UID_RED, 0L, 0L, 0L, 0L, 0); + + // 5g network comes online. + incrementCurrentTime(MINUTE_IN_MILLIS); + setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_NR); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + // Existing stats remains. + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 16L, 22L, 17L, 2L, 0L)) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, + 33L, 27L, 8L, 10L, 1L)) + // Add some traffic on 5g. + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 5L, 13L, 31L, 9L, 2L))); + forcePollAndWaitForIdle(); + + // Verify ALL_MOBILE template gets all. + assertUidTotal(sTemplateImsi1, UID_RED, 54L, 62L, 56L, 21L, 3); + // 3g/4g template counters do not increase. + assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0); + assertUidTotal(template4g, UID_RED, 4L + 33L, 4L + 27L, 3L + 8L, 1L + 10L, 1); + // Verify 5g template gets the 5g count. + assertUidTotal(template5g, UID_RED, 5L, 13L, 31L, 9L, 2); + } + + @Test + public void testMobileStatsOemManaged() throws Exception { + final NetworkTemplate templateOemPaid = new NetworkTemplate(MATCH_MOBILE_WILDCARD, + /*subscriberId=*/null, /*matchSubscriberIds=*/null, /*networkId=*/null, + METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, OEM_PAID, + SUBSCRIBER_ID_MATCH_RULE_EXACT); + + final NetworkTemplate templateOemPrivate = new NetworkTemplate(MATCH_MOBILE_WILDCARD, + /*subscriberId=*/null, /*matchSubscriberIds=*/null, /*networkId=*/null, + METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, OEM_PRIVATE, + SUBSCRIBER_ID_MATCH_RULE_EXACT); + + final NetworkTemplate templateOemAll = new NetworkTemplate(MATCH_MOBILE_WILDCARD, + /*subscriberId=*/null, /*matchSubscriberIds=*/null, /*networkId=*/null, + METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, + OEM_PAID | OEM_PRIVATE, SUBSCRIBER_ID_MATCH_RULE_EXACT); + + final NetworkTemplate templateOemYes = new NetworkTemplate(MATCH_MOBILE_WILDCARD, + /*subscriberId=*/null, /*matchSubscriberIds=*/null, /*networkId=*/null, + METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, OEM_MANAGED_YES, + SUBSCRIBER_ID_MATCH_RULE_EXACT); + + final NetworkTemplate templateOemNone = new NetworkTemplate(MATCH_MOBILE_WILDCARD, + /*subscriberId=*/null, /*matchSubscriberIds=*/null, /*networkId=*/null, + METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, OEM_MANAGED_NO, + SUBSCRIBER_ID_MATCH_RULE_EXACT); + + // OEM_PAID network comes online. + NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{ + buildOemManagedMobileState(IMSI_1, false, + new int[]{NetworkCapabilities.NET_CAPABILITY_OEM_PAID})}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Create some traffic. + incrementCurrentTime(MINUTE_IN_MILLIS); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 36L, 41L, 24L, 96L, 0L))); + forcePollAndWaitForIdle(); + + // OEM_PRIVATE network comes online. + states = new NetworkStateSnapshot[]{buildOemManagedMobileState(IMSI_1, false, + new int[]{NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE})}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Create some traffic. + incrementCurrentTime(MINUTE_IN_MILLIS); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 49L, 71L, 72L, 48L, 0L))); + forcePollAndWaitForIdle(); + + // OEM_PAID + OEM_PRIVATE network comes online. + states = new NetworkStateSnapshot[]{buildOemManagedMobileState(IMSI_1, false, + new int[]{NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE, + NetworkCapabilities.NET_CAPABILITY_OEM_PAID})}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Create some traffic. + incrementCurrentTime(MINUTE_IN_MILLIS); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 57L, 86L, 83L, 93L, 0L))); + forcePollAndWaitForIdle(); + + // OEM_NONE network comes online. + states = new NetworkStateSnapshot[]{buildOemManagedMobileState(IMSI_1, false, new int[]{})}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Create some traffic. + incrementCurrentTime(MINUTE_IN_MILLIS); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 29L, 73L, 34L, 31L, 0L))); + forcePollAndWaitForIdle(); + + // Verify OEM_PAID template gets only relevant stats. + assertUidTotal(templateOemPaid, UID_RED, 36L, 41L, 24L, 96L, 0); + + // Verify OEM_PRIVATE template gets only relevant stats. + assertUidTotal(templateOemPrivate, UID_RED, 49L, 71L, 72L, 48L, 0); + + // Verify OEM_PAID + OEM_PRIVATE template gets only relevant stats. + assertUidTotal(templateOemAll, UID_RED, 57L, 86L, 83L, 93L, 0); + + // Verify OEM_NONE sees only non-OEM managed stats. + assertUidTotal(templateOemNone, UID_RED, 29L, 73L, 34L, 31L, 0); + + // Verify OEM_MANAGED_YES sees all OEM managed stats. + assertUidTotal(templateOemYes, UID_RED, + 36L + 49L + 57L, + 41L + 71L + 86L, + 24L + 72L + 83L, + 96L + 48L + 93L, 0); + + // Verify ALL_MOBILE template gets both OEM managed and non-OEM managed stats. + assertUidTotal(sTemplateImsi1, UID_RED, + 36L + 49L + 57L + 29L, + 41L + 71L + 86L + 73L, + 24L + 72L + 83L + 34L, + 96L + 48L + 93L + 31L, 0); + } + + // TODO: support per IMSI state + private void setMobileRatTypeAndWaitForIdle(int ratType) { + when(mNetworkStatsSubscriptionsMonitor.getRatTypeForSubscriberId(anyString())) + .thenReturn(ratType); + mService.handleOnCollapsedRatTypeChanged(); + HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT); + } + + @Test + public void testSummaryForAllUid() throws Exception { + // pretend that network comes online + expectDefaultSettings(); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // create some traffic for two apps + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 50L, 5L, 50L, 5L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 10L, 1L, 10L, 1L, 0L) + .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 1024L, 8L, 512L, 4L, 0L)); + mService.incrementOperationCount(UID_RED, 0xF00D, 1); + + forcePollAndWaitForIdle(); + + // verify service recorded history + assertUidTotal(sTemplateWifi, UID_RED, 50L, 5L, 50L, 5L, 1); + assertUidTotal(sTemplateWifi, UID_BLUE, 1024L, 8L, 512L, 4L, 0); + + + // now create more traffic in next hour, but only for one app + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 50L, 5L, 50L, 5L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 10L, 1L, 10L, 1L, 0L) + .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, + 2048L, 16L, 1024L, 8L, 0L)); + forcePollAndWaitForIdle(); + + // first verify entire history present + NetworkStats stats = mSession.getSummaryForAllUid( + sTemplateWifi, Long.MIN_VALUE, Long.MAX_VALUE, true); + assertEquals(3, stats.size()); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 50L, 5L, 50L, 5L, 1); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 10L, 1L, 10L, 1L, 1); + assertValues(stats, IFACE_ALL, UID_BLUE, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 2048L, 16L, 1024L, 8L, 0); + + // now verify that recent history only contains one uid + final long currentTime = currentTimeMillis(); + stats = mSession.getSummaryForAllUid( + sTemplateWifi, currentTime - HOUR_IN_MILLIS, currentTime, true); + assertEquals(1, stats.size()); + assertValues(stats, IFACE_ALL, UID_BLUE, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 1024L, 8L, 512L, 4L, 0); + } + + @Test + public void testDetailedUidStats() throws Exception { + // pretend that network comes online + expectDefaultSettings(); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + NetworkStats.Entry entry1 = new NetworkStats.Entry( + TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 50L, 5L, 50L, 5L, 0L); + NetworkStats.Entry entry2 = new NetworkStats.Entry( + TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 50L, 5L, 50L, 5L, 0L); + NetworkStats.Entry entry3 = new NetworkStats.Entry( + TEST_IFACE, UID_BLUE, SET_DEFAULT, 0xBEEF, 1024L, 8L, 512L, 4L, 0L); + + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3) + .insertEntry(entry1) + .insertEntry(entry2) + .insertEntry(entry3)); + mService.incrementOperationCount(UID_RED, 0xF00D, 1); + + NetworkStats stats = mService.getDetailedUidStats(INTERFACES_ALL); + + assertEquals(3, stats.size()); + entry1.operations = 1; + assertEquals(entry1, stats.getValues(0, null)); + entry2.operations = 1; + assertEquals(entry2, stats.getValues(1, null)); + assertEquals(entry3, stats.getValues(2, null)); + } + + @Test + public void testDetailedUidStats_Filtered() throws Exception { + // pretend that network comes online + expectDefaultSettings(); + + final String stackedIface = "stacked-test0"; + final LinkProperties stackedProp = new LinkProperties(); + stackedProp.setInterfaceName(stackedIface); + final NetworkStateSnapshot wifiState = buildWifiState(); + wifiState.getLinkProperties().addStackedLink(stackedProp); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {wifiState}; + + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + NetworkStats.Entry uidStats = new NetworkStats.Entry( + TEST_IFACE, UID_BLUE, SET_DEFAULT, 0xF00D, 1024L, 8L, 512L, 4L, 0L); + // Stacked on matching interface + NetworkStats.Entry tetheredStats1 = new NetworkStats.Entry( + stackedIface, UID_BLUE, SET_DEFAULT, 0xF00D, 1024L, 8L, 512L, 4L, 0L); + // Different interface + NetworkStats.Entry tetheredStats2 = new NetworkStats.Entry( + "otherif", UID_BLUE, SET_DEFAULT, 0xF00D, 1024L, 8L, 512L, 4L, 0L); + + final String[] ifaceFilter = new String[] { TEST_IFACE }; + final String[] augmentedIfaceFilter = new String[] { stackedIface, TEST_IFACE }; + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(buildEmptyStats()); + when(mStatsFactory.augmentWithStackedInterfaces(eq(ifaceFilter))) + .thenReturn(augmentedIfaceFilter); + when(mStatsFactory.readNetworkStatsDetail(eq(UID_ALL), any(), eq(TAG_ALL))) + .thenReturn(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(uidStats)); + when(mNetManager.getNetworkStatsTethering(STATS_PER_UID)) + .thenReturn(new NetworkStats(getElapsedRealtime(), 2) + .insertEntry(tetheredStats1) + .insertEntry(tetheredStats2)); + + NetworkStats stats = mService.getDetailedUidStats(ifaceFilter); + + // mStatsFactory#readNetworkStatsDetail() has the following invocations: + // 1) NetworkStatsService#systemReady from #setUp. + // 2) mService#forceUpdateIfaces in the test above. + // + // Additionally, we should have one call from the above call to mService#getDetailedUidStats + // with the augmented ifaceFilter. + verify(mStatsFactory, times(2)).readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL); + verify(mStatsFactory, times(1)).readNetworkStatsDetail( + eq(UID_ALL), + eq(augmentedIfaceFilter), + eq(TAG_ALL)); + assertTrue(ArrayUtils.contains(stats.getUniqueIfaces(), TEST_IFACE)); + assertTrue(ArrayUtils.contains(stats.getUniqueIfaces(), stackedIface)); + assertEquals(2, stats.size()); + assertEquals(uidStats, stats.getValues(0, null)); + assertEquals(tetheredStats1, stats.getValues(1, null)); + } + + @Test + public void testForegroundBackground() throws Exception { + // pretend that network comes online + expectDefaultSettings(); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // create some initial traffic + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 128L, 2L, 128L, 2L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 64L, 1L, 64L, 1L, 0L)); + mService.incrementOperationCount(UID_RED, 0xF00D, 1); + + forcePollAndWaitForIdle(); + + // verify service recorded history + assertUidTotal(sTemplateWifi, UID_RED, 128L, 2L, 128L, 2L, 1); + + + // now switch to foreground + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 128L, 2L, 128L, 2L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 64L, 1L, 64L, 1L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, 32L, 2L, 32L, 2L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, 0xFAAD, 1L, 1L, 1L, 1L, 0L)); + mService.setUidForeground(UID_RED, true); + mService.incrementOperationCount(UID_RED, 0xFAAD, 1); + + forcePollAndWaitForIdle(); + + // test that we combined correctly + assertUidTotal(sTemplateWifi, UID_RED, 160L, 4L, 160L, 4L, 2); + + // verify entire history present + final NetworkStats stats = mSession.getSummaryForAllUid( + sTemplateWifi, Long.MIN_VALUE, Long.MAX_VALUE, true); + assertEquals(4, stats.size()); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 1); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 64L, 1L, 64L, 1L, 1); + assertValues(stats, IFACE_ALL, UID_RED, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 32L, 2L, 32L, 2L, 1); + assertValues(stats, IFACE_ALL, UID_RED, SET_FOREGROUND, 0xFAAD, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 1L, 1L, 1L, 1L, 1); + } + + @Test + public void testMetered() throws Exception { + // pretend that network comes online + expectDefaultSettings(); + NetworkStateSnapshot[] states = + new NetworkStateSnapshot[] {buildWifiState(true /* isMetered */, TEST_IFACE)}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // create some initial traffic + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(buildEmptyStats()); + // Note that all traffic from NetworkManagementService is tagged as METERED_NO, ROAMING_NO + // and DEFAULT_NETWORK_YES, because these three properties aren't tracked at that layer. + // We layer them on top by inspecting the iface properties. + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 64L, 1L, 64L, 1L, 0L)); + mService.incrementOperationCount(UID_RED, 0xF00D, 1); + + forcePollAndWaitForIdle(); + + // verify service recorded history + assertUidTotal(sTemplateWifi, UID_RED, 128L, 2L, 128L, 2L, 1); + // verify entire history present + final NetworkStats stats = mSession.getSummaryForAllUid( + sTemplateWifi, Long.MIN_VALUE, Long.MAX_VALUE, true); + assertEquals(2, stats.size()); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 1); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 64L, 1L, 64L, 1L, 1); + } + + @Test + public void testRoaming() throws Exception { + // pretend that network comes online + expectDefaultSettings(); + NetworkStateSnapshot[] states = + new NetworkStateSnapshot[] {buildMobile3gState(IMSI_1, true /* isRoaming */)}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Create some traffic + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(buildEmptyStats()); + // Note that all traffic from NetworkManagementService is tagged as METERED_NO and + // ROAMING_NO, because metered and roaming isn't tracked at that layer. We layer it + // on top by inspecting the iface properties. + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_ALL, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, METERED_ALL, ROAMING_NO, + DEFAULT_NETWORK_YES, 64L, 1L, 64L, 1L, 0L)); + forcePollAndWaitForIdle(); + + // verify service recorded history + assertUidTotal(sTemplateImsi1, UID_RED, 128L, 2L, 128L, 2L, 0); + + // verify entire history present + final NetworkStats stats = mSession.getSummaryForAllUid( + sTemplateImsi1, Long.MIN_VALUE, Long.MAX_VALUE, true); + assertEquals(2, stats.size()); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_ALL, ROAMING_YES, + DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 0); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, METERED_ALL, ROAMING_YES, + DEFAULT_NETWORK_YES, 64L, 1L, 64L, 1L, 0); + } + + @Test + public void testTethering() throws Exception { + // pretend first mobile network comes online + expectDefaultSettings(); + final NetworkStateSnapshot[] states = + new NetworkStateSnapshot[]{buildMobile3gState(IMSI_1)}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // create some tethering traffic + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + + // Register custom provider and retrieve callback. + final TestableNetworkStatsProviderBinder provider = + new TestableNetworkStatsProviderBinder(); + final INetworkStatsProviderCallback cb = + mService.registerNetworkStatsProvider("TEST-TETHERING-OFFLOAD", provider); + assertNotNull(cb); + final long now = getElapsedRealtime(); + + // Traffic seen by kernel counters (includes software tethering). + final NetworkStats swIfaceStats = new NetworkStats(now, 1) + .insertEntry(TEST_IFACE, 1536L, 12L, 384L, 3L); + // Hardware tethering traffic, not seen by kernel counters. + final NetworkStats tetherHwIfaceStats = new NetworkStats(now, 1) + .insertEntry(new NetworkStats.Entry(TEST_IFACE, UID_ALL, SET_DEFAULT, + TAG_NONE, METERED_YES, ROAMING_NO, DEFAULT_NETWORK_YES, + 512L, 4L, 128L, 1L, 0L)); + final NetworkStats tetherHwUidStats = new NetworkStats(now, 1) + .insertEntry(new NetworkStats.Entry(TEST_IFACE, UID_TETHERING, SET_DEFAULT, + TAG_NONE, METERED_YES, ROAMING_NO, DEFAULT_NETWORK_YES, + 512L, 4L, 128L, 1L, 0L)); + cb.notifyStatsUpdated(0 /* unused */, tetherHwIfaceStats, tetherHwUidStats); + + // Fake some traffic done by apps on the device (as opposed to tethering), and record it + // into UID stats (as opposed to iface stats). + final NetworkStats localUidStats = new NetworkStats(now, 1) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 128L, 2L, 128L, 2L, 0L); + // Software per-uid tethering traffic. + final NetworkStats tetherSwUidStats = new NetworkStats(now, 1) + .insertEntry(TEST_IFACE, UID_TETHERING, SET_DEFAULT, TAG_NONE, 1408L, 10L, 256L, 1L, + 0L); + + expectNetworkStatsSummary(swIfaceStats); + expectNetworkStatsUidDetail(localUidStats, tetherSwUidStats); + forcePollAndWaitForIdle(); + + // verify service recorded history + assertNetworkTotal(sTemplateImsi1, 2048L, 16L, 512L, 4L, 0); + assertUidTotal(sTemplateImsi1, UID_RED, 128L, 2L, 128L, 2L, 0); + assertUidTotal(sTemplateImsi1, UID_TETHERING, 1920L, 14L, 384L, 2L, 0); + } + + @Test + public void testRegisterUsageCallback() throws Exception { + // pretend that wifi network comes online; service should ask about full + // network state, and poll any existing interfaces before updating. + expectDefaultSettings(); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // verify service has empty history for wifi + assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0); + long thresholdInBytes = 1L; // very small; should be overriden by framework + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, thresholdInBytes); + + // Create a messenger that waits for callback activity + ConditionVariable cv = new ConditionVariable(false); + LatchedHandler latchedHandler = new LatchedHandler(Looper.getMainLooper(), cv); + Messenger messenger = new Messenger(latchedHandler); + + // Force poll + expectDefaultSettings(); + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + // Register and verify request and that binder was called + DataUsageRequest request = + mService.registerUsageCallback(mServiceContext.getOpPackageName(), inputRequest, + messenger, mBinder); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateWifi, request.template)); + long minThresholdInBytes = 2 * 1024 * 1024; // 2 MB + assertEquals(minThresholdInBytes, request.thresholdInBytes); + + HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT); + + // Make sure that the caller binder gets connected + verify(mBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt()); + + // modify some number on wifi, and trigger poll event + // not enough traffic to call data usage callback + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, 1024L, 1L, 2048L, 2L)); + expectNetworkStatsUidDetail(buildEmptyStats()); + forcePollAndWaitForIdle(); + + // verify service recorded history + assertNetworkTotal(sTemplateWifi, 1024L, 1L, 2048L, 2L, 0); + + // make sure callback has not being called + assertEquals(INVALID_TYPE, latchedHandler.lastMessageType); + + // and bump forward again, with counters going higher. this is + // important, since it will trigger the data usage callback + incrementCurrentTime(DAY_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, 4096000L, 4L, 8192000L, 8L)); + expectNetworkStatsUidDetail(buildEmptyStats()); + forcePollAndWaitForIdle(); + + // verify service recorded history + assertNetworkTotal(sTemplateWifi, 4096000L, 4L, 8192000L, 8L, 0); + + + // Wait for the caller to ack receipt of CALLBACK_LIMIT_REACHED + assertTrue(cv.block(WAIT_TIMEOUT)); + assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, latchedHandler.lastMessageType); + cv.close(); + + // Allow binder to disconnect + when(mBinder.unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt())).thenReturn(true); + + // Unregister request + mService.unregisterUsageRequest(request); + + // Wait for the caller to ack receipt of CALLBACK_RELEASED + assertTrue(cv.block(WAIT_TIMEOUT)); + assertEquals(NetworkStatsManager.CALLBACK_RELEASED, latchedHandler.lastMessageType); + + // Make sure that the caller binder gets disconnected + verify(mBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt()); + } + + @Test + public void testUnregisterUsageCallback_unknown_noop() throws Exception { + String callingPackage = "the.calling.package"; + long thresholdInBytes = 10 * 1024 * 1024; // 10 MB + DataUsageRequest unknownRequest = new DataUsageRequest( + 2 /* requestId */, sTemplateImsi1, thresholdInBytes); + + mService.unregisterUsageRequest(unknownRequest); + } + + @Test + public void testStatsProviderUpdateStats() throws Exception { + // Pretend that network comes online. + expectDefaultSettings(); + final NetworkStateSnapshot[] states = + new NetworkStateSnapshot[]{buildWifiState(true /* isMetered */, TEST_IFACE)}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + // Register custom provider and retrieve callback. + final TestableNetworkStatsProviderBinder provider = + new TestableNetworkStatsProviderBinder(); + final INetworkStatsProviderCallback cb = + mService.registerNetworkStatsProvider("TEST", provider); + assertNotNull(cb); + + mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Verifies that one requestStatsUpdate will be called during iface update. + provider.expectOnRequestStatsUpdate(0 /* unused */); + + // Create some initial traffic and report to the service. + incrementCurrentTime(HOUR_IN_MILLIS); + final NetworkStats expectedStats = new NetworkStats(0L, 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, + TAG_NONE, METERED_YES, ROAMING_NO, DEFAULT_NETWORK_YES, + 128L, 2L, 128L, 2L, 1L)) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, + 0xF00D, METERED_YES, ROAMING_NO, DEFAULT_NETWORK_YES, + 64L, 1L, 64L, 1L, 1L)); + cb.notifyStatsUpdated(0 /* unused */, expectedStats, expectedStats); + + // Make another empty mutable stats object. This is necessary since the new NetworkStats + // object will be used to compare with the old one in NetworkStatsRecoder, two of them + // cannot be the same object. + expectNetworkStatsUidDetail(buildEmptyStats()); + + forcePollAndWaitForIdle(); + + // Verifies that one requestStatsUpdate and setAlert will be called during polling. + provider.expectOnRequestStatsUpdate(0 /* unused */); + provider.expectOnSetAlert(MB_IN_BYTES); + + // Verifies that service recorded history, does not verify uid tag part. + assertUidTotal(sTemplateWifi, UID_RED, 128L, 2L, 128L, 2L, 1); + + // Verifies that onStatsUpdated updates the stats accordingly. + final NetworkStats stats = mSession.getSummaryForAllUid( + sTemplateWifi, Long.MIN_VALUE, Long.MAX_VALUE, true); + assertEquals(2, stats.size()); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 1L); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 64L, 1L, 64L, 1L, 1L); + + // Verifies that unregister the callback will remove the provider from service. + cb.unregister(); + forcePollAndWaitForIdle(); + provider.assertNoCallback(); + } + + @Test + public void testDualVilteProviderStats() throws Exception { + // Pretend that network comes online. + expectDefaultSettings(); + final int subId1 = 1; + final int subId2 = 2; + final NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{ + buildImsState(IMSI_1, subId1, TEST_IFACE), + buildImsState(IMSI_2, subId2, TEST_IFACE2)}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + // Register custom provider and retrieve callback. + final TestableNetworkStatsProviderBinder provider = + new TestableNetworkStatsProviderBinder(); + final INetworkStatsProviderCallback cb = + mService.registerNetworkStatsProvider("TEST", provider); + assertNotNull(cb); + + mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Verifies that one requestStatsUpdate will be called during iface update. + provider.expectOnRequestStatsUpdate(0 /* unused */); + + // Create some initial traffic and report to the service. + incrementCurrentTime(HOUR_IN_MILLIS); + final String vtIface1 = NetworkStats.IFACE_VT + subId1; + final String vtIface2 = NetworkStats.IFACE_VT + subId2; + final NetworkStats expectedStats = new NetworkStats(0L, 1) + .addEntry(new NetworkStats.Entry(vtIface1, UID_RED, SET_DEFAULT, + TAG_NONE, METERED_YES, ROAMING_NO, DEFAULT_NETWORK_YES, + 128L, 2L, 128L, 2L, 1L)) + .addEntry(new NetworkStats.Entry(vtIface2, UID_RED, SET_DEFAULT, + TAG_NONE, METERED_YES, ROAMING_NO, DEFAULT_NETWORK_YES, + 64L, 1L, 64L, 1L, 1L)); + cb.notifyStatsUpdated(0 /* unused */, expectedStats, expectedStats); + + // Make another empty mutable stats object. This is necessary since the new NetworkStats + // object will be used to compare with the old one in NetworkStatsRecoder, two of them + // cannot be the same object. + expectNetworkStatsUidDetail(buildEmptyStats()); + + forcePollAndWaitForIdle(); + + // Verifies that one requestStatsUpdate and setAlert will be called during polling. + provider.expectOnRequestStatsUpdate(0 /* unused */); + provider.expectOnSetAlert(MB_IN_BYTES); + + // Verifies that service recorded history, does not verify uid tag part. + assertUidTotal(sTemplateImsi1, UID_RED, 128L, 2L, 128L, 2L, 1); + + // Verifies that onStatsUpdated updates the stats accordingly. + NetworkStats stats = mSession.getSummaryForAllUid( + sTemplateImsi1, Long.MIN_VALUE, Long.MAX_VALUE, true); + assertEquals(1, stats.size()); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 1L); + + stats = mSession.getSummaryForAllUid( + sTemplateImsi2, Long.MIN_VALUE, Long.MAX_VALUE, true); + assertEquals(1, stats.size()); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 64L, 1L, 64L, 1L, 1L); + + // Verifies that unregister the callback will remove the provider from service. + cb.unregister(); + forcePollAndWaitForIdle(); + provider.assertNoCallback(); + } + + @Test + public void testStatsProviderSetAlert() throws Exception { + // Pretend that network comes online. + expectDefaultSettings(); + NetworkStateSnapshot[] states = + new NetworkStateSnapshot[]{buildWifiState(true /* isMetered */, TEST_IFACE)}; + mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Register custom provider and retrieve callback. + final TestableNetworkStatsProviderBinder provider = + new TestableNetworkStatsProviderBinder(); + final INetworkStatsProviderCallback cb = + mService.registerNetworkStatsProvider("TEST", provider); + assertNotNull(cb); + + // Simulates alert quota of the provider has been reached. + cb.notifyAlertReached(); + HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT); + + // Verifies that polling is triggered by alert reached. + provider.expectOnRequestStatsUpdate(0 /* unused */); + // Verifies that global alert will be re-armed. + provider.expectOnSetAlert(MB_IN_BYTES); + } + + private void setCombineSubtypeEnabled(boolean enable) { + when(mSettings.getCombineSubtypeEnabled()).thenReturn(enable); + mHandler.post(() -> mContentObserver.onChange(false, Settings.Global + .getUriFor(Settings.Global.NETSTATS_COMBINE_SUBTYPE_ENABLED))); + waitForIdle(); + if (enable) { + verify(mNetworkStatsSubscriptionsMonitor).stop(); + } else { + verify(mNetworkStatsSubscriptionsMonitor).start(); + } + } + + @Test + public void testDynamicWatchForNetworkRatTypeChanges() throws Exception { + // Build 3G template, type unknown template to get stats while network type is unknown + // and type all template to get the sum of all network type stats. + final NetworkTemplate template3g = + buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UMTS); + final NetworkTemplate templateUnknown = + buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UNKNOWN); + final NetworkTemplate templateAll = + buildTemplateMobileWithRatType(null, NETWORK_TYPE_ALL); + final NetworkStateSnapshot[] states = + new NetworkStateSnapshot[]{buildMobile3gState(IMSI_1)}; + + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + // 3G network comes online. + setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_UMTS); + mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Create some traffic. + incrementCurrentTime(MINUTE_IN_MILLIS); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 12L, 18L, 14L, 1L, 0L))); + forcePollAndWaitForIdle(); + + // Since CombineSubtypeEnabled is false by default in unit test, the generated traffic + // will be split by RAT type. Verify 3G templates gets stats, while template with unknown + // RAT type gets nothing, and template with NETWORK_TYPE_ALL gets all stats. + assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0); + assertUidTotal(templateUnknown, UID_RED, 0L, 0L, 0L, 0L, 0); + assertUidTotal(templateAll, UID_RED, 12L, 18L, 14L, 1L, 0); + + // Stop monitoring data usage per RAT type changes NetworkStatsService records data + // to {@link TelephonyManager#NETWORK_TYPE_UNKNOWN}. + setCombineSubtypeEnabled(true); + + // Call handleOnCollapsedRatTypeChanged manually to simulate the callback fired + // when stopping monitor, this is needed by NetworkStatsService to trigger updateIfaces. + mService.handleOnCollapsedRatTypeChanged(); + HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT); + // Create some traffic. + incrementCurrentTime(MINUTE_IN_MILLIS); + // Append more traffic on existing snapshot. + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 12L + 4L, 18L + 4L, 14L + 3L, 1L + 1L, 0L)) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, + 35L, 29L, 7L, 11L, 1L))); + forcePollAndWaitForIdle(); + + // Verify 3G counters do not increase, while template with unknown RAT type gets new + // traffic and template with NETWORK_TYPE_ALL gets all stats. + assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0); + assertUidTotal(templateUnknown, UID_RED, 4L + 35L, 4L + 29L, 3L + 7L, 1L + 11L, 1); + assertUidTotal(templateAll, UID_RED, 16L + 35L, 22L + 29L, 17L + 7L, 2L + 11L, 1); + + // Start monitoring data usage per RAT type changes and NetworkStatsService records data + // by a granular subtype representative of the actual subtype + setCombineSubtypeEnabled(false); + + mService.handleOnCollapsedRatTypeChanged(); + HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT); + // Create some traffic. + incrementCurrentTime(MINUTE_IN_MILLIS); + // Append more traffic on existing snapshot. + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 22L, 26L, 19L, 5L, 0L)) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, + 35L, 29L, 7L, 11L, 1L))); + forcePollAndWaitForIdle(); + + // Verify traffic is split by RAT type, no increase on template with unknown RAT type + // and template with NETWORK_TYPE_ALL gets all stats. + assertUidTotal(template3g, UID_RED, 6L + 12L , 4L + 18L, 2L + 14L, 3L + 1L, 0); + assertUidTotal(templateUnknown, UID_RED, 4L + 35L, 4L + 29L, 3L + 7L, 1L + 11L, 1); + assertUidTotal(templateAll, UID_RED, 22L + 35L, 26L + 29L, 19L + 7L, 5L + 11L, 1); + } + + @Test + public void testOperationCount_nonDefault_traffic() throws Exception { + // Pretend mobile network comes online, but wifi is the default network. + expectDefaultSettings(); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{ + buildWifiState(true /*isMetered*/, TEST_IFACE2), buildMobile3gState(IMSI_1)}; + expectNetworkStatsUidDetail(buildEmptyStats()); + mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Create some traffic on mobile network. + incrementCurrentTime(HOUR_IN_MILLIS); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 4) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 2L, 1L, 3L, 4L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 1L, 3L, 2L, 1L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 5L, 4L, 1L, 4L, 0L)); + // Increment operation count, which must have a specific tag. + mService.incrementOperationCount(UID_RED, 0xF00D, 2); + forcePollAndWaitForIdle(); + + // Verify mobile summary is not changed by the operation count. + final NetworkTemplate templateMobile = + buildTemplateMobileWithRatType(null, NETWORK_TYPE_ALL); + final NetworkStats statsMobile = mSession.getSummaryForAllUid( + templateMobile, Long.MIN_VALUE, Long.MAX_VALUE, true); + assertValues(statsMobile, IFACE_ALL, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, + DEFAULT_NETWORK_ALL, 3L, 4L, 5L, 5L, 0); + assertValues(statsMobile, IFACE_ALL, UID_RED, SET_ALL, 0xF00D, METERED_ALL, ROAMING_ALL, + DEFAULT_NETWORK_ALL, 5L, 4L, 1L, 4L, 0); + + // Verify the operation count is blamed onto the default network. + // TODO: Blame onto the default network is not very reasonable. Consider blame onto the + // network that generates the traffic. + final NetworkTemplate templateWifi = buildTemplateWifiWildcard(); + final NetworkStats statsWifi = mSession.getSummaryForAllUid( + templateWifi, Long.MIN_VALUE, Long.MAX_VALUE, true); + assertValues(statsWifi, IFACE_ALL, UID_RED, SET_ALL, 0xF00D, METERED_ALL, ROAMING_ALL, + DEFAULT_NETWORK_ALL, 0L, 0L, 0L, 0L, 2); + } + + private static File getBaseDir(File statsDir) { + File baseDir = new File(statsDir, "netstats"); + baseDir.mkdirs(); + return baseDir; + } + + private void assertNetworkTotal(NetworkTemplate template, long rxBytes, long rxPackets, + long txBytes, long txPackets, int operations) throws Exception { + assertNetworkTotal(template, Long.MIN_VALUE, Long.MAX_VALUE, rxBytes, rxPackets, txBytes, + txPackets, operations); + } + + private void assertNetworkTotal(NetworkTemplate template, long start, long end, long rxBytes, + long rxPackets, long txBytes, long txPackets, int operations) throws Exception { + // verify history API + final NetworkStatsHistory history = mSession.getHistoryForNetwork(template, FIELD_ALL); + assertValues(history, start, end, rxBytes, rxPackets, txBytes, txPackets, operations); + + // verify summary API + final NetworkStats stats = mSession.getSummaryForNetwork(template, start, end); + assertValues(stats, IFACE_ALL, UID_ALL, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, + DEFAULT_NETWORK_ALL, rxBytes, rxPackets, txBytes, txPackets, operations); + } + + private void assertUidTotal(NetworkTemplate template, int uid, long rxBytes, long rxPackets, + long txBytes, long txPackets, int operations) throws Exception { + assertUidTotal(template, uid, SET_ALL, METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, + rxBytes, rxPackets, txBytes, txPackets, operations); + } + + private void assertUidTotal(NetworkTemplate template, int uid, int set, int metered, + int roaming, int defaultNetwork, long rxBytes, long rxPackets, long txBytes, + long txPackets, int operations) throws Exception { + // verify history API + final NetworkStatsHistory history = mSession.getHistoryForUid( + template, uid, set, TAG_NONE, FIELD_ALL); + assertValues(history, Long.MIN_VALUE, Long.MAX_VALUE, rxBytes, rxPackets, txBytes, + txPackets, operations); + + // verify summary API + final NetworkStats stats = mSession.getSummaryForAllUid( + template, Long.MIN_VALUE, Long.MAX_VALUE, false); + assertValues(stats, IFACE_ALL, uid, set, TAG_NONE, metered, roaming, defaultNetwork, + rxBytes, rxPackets, txBytes, txPackets, operations); + } + + private void expectSystemReady() throws Exception { + expectNetworkStatsSummary(buildEmptyStats()); + } + + private String getActiveIface(NetworkStateSnapshot... states) throws Exception { + if (states == null || states.length == 0 || states[0].getLinkProperties() == null) { + return null; + } + return states[0].getLinkProperties().getInterfaceName(); + } + + private void expectNetworkStatsSummary(NetworkStats summary) throws Exception { + expectNetworkStatsSummaryDev(summary.clone()); + expectNetworkStatsSummaryXt(summary.clone()); + } + + private void expectNetworkStatsSummaryDev(NetworkStats summary) throws Exception { + when(mStatsFactory.readNetworkStatsSummaryDev()).thenReturn(summary); + } + + private void expectNetworkStatsSummaryXt(NetworkStats summary) throws Exception { + when(mStatsFactory.readNetworkStatsSummaryXt()).thenReturn(summary); + } + + private void expectNetworkStatsUidDetail(NetworkStats detail) throws Exception { + expectNetworkStatsUidDetail(detail, new NetworkStats(0L, 0)); + } + + private void expectNetworkStatsUidDetail(NetworkStats detail, NetworkStats tetherStats) + throws Exception { + when(mStatsFactory.readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL)) + .thenReturn(detail); + + // also include tethering details, since they are folded into UID + when(mNetManager.getNetworkStatsTethering(STATS_PER_UID)).thenReturn(tetherStats); + } + + private void expectDefaultSettings() throws Exception { + expectSettings(0L, HOUR_IN_MILLIS, WEEK_IN_MILLIS); + } + + private void expectSettings(long persistBytes, long bucketDuration, long deleteAge) + throws Exception { + when(mSettings.getPollInterval()).thenReturn(HOUR_IN_MILLIS); + when(mSettings.getPollDelay()).thenReturn(0L); + when(mSettings.getSampleEnabled()).thenReturn(true); + when(mSettings.getCombineSubtypeEnabled()).thenReturn(false); + + final Config config = new Config(bucketDuration, deleteAge, deleteAge); + when(mSettings.getDevConfig()).thenReturn(config); + when(mSettings.getXtConfig()).thenReturn(config); + when(mSettings.getUidConfig()).thenReturn(config); + when(mSettings.getUidTagConfig()).thenReturn(config); + + when(mSettings.getGlobalAlertBytes(anyLong())).thenReturn(MB_IN_BYTES); + when(mSettings.getDevPersistBytes(anyLong())).thenReturn(MB_IN_BYTES); + when(mSettings.getXtPersistBytes(anyLong())).thenReturn(MB_IN_BYTES); + when(mSettings.getUidPersistBytes(anyLong())).thenReturn(MB_IN_BYTES); + when(mSettings.getUidTagPersistBytes(anyLong())).thenReturn(MB_IN_BYTES); + } + + private void assertStatsFilesExist(boolean exist) { + final File basePath = new File(mStatsDir, "netstats"); + if (exist) { + assertTrue(basePath.list().length > 0); + } else { + assertTrue(basePath.list().length == 0); + } + } + + private static void assertValues(NetworkStatsHistory stats, long start, long end, long rxBytes, + long rxPackets, long txBytes, long txPackets, int operations) { + final NetworkStatsHistory.Entry entry = stats.getValues(start, end, null); + assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes); + assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets); + assertEquals("unexpected txBytes", txBytes, entry.txBytes); + assertEquals("unexpected txPackets", txPackets, entry.txPackets); + assertEquals("unexpected operations", operations, entry.operations); + } + + private static NetworkStateSnapshot buildWifiState() { + return buildWifiState(false, TEST_IFACE); + } + + private static NetworkStateSnapshot buildWifiState(boolean isMetered, @NonNull String iface) { + final LinkProperties prop = new LinkProperties(); + prop.setInterfaceName(iface); + final NetworkCapabilities capabilities = new NetworkCapabilities(); + capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, !isMetered); + capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, true); + capabilities.addTransportType(NetworkCapabilities.TRANSPORT_WIFI); + capabilities.setSSID(TEST_SSID); + return new NetworkStateSnapshot(WIFI_NETWORK, capabilities, prop, null, TYPE_WIFI); + } + + private static NetworkStateSnapshot buildMobile3gState(String subscriberId) { + return buildMobile3gState(subscriberId, false /* isRoaming */); + } + + private static NetworkStateSnapshot buildMobile3gState(String subscriberId, boolean isRoaming) { + final LinkProperties prop = new LinkProperties(); + prop.setInterfaceName(TEST_IFACE); + final NetworkCapabilities capabilities = new NetworkCapabilities(); + capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, false); + capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, !isRoaming); + capabilities.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR); + return new NetworkStateSnapshot( + MOBILE_NETWORK, capabilities, prop, subscriberId, TYPE_MOBILE); + } + + private NetworkStats buildEmptyStats() { + return new NetworkStats(getElapsedRealtime(), 0); + } + + private static NetworkStateSnapshot buildOemManagedMobileState( + String subscriberId, boolean isRoaming, int[] oemNetCapabilities) { + final LinkProperties prop = new LinkProperties(); + prop.setInterfaceName(TEST_IFACE); + final NetworkCapabilities capabilities = new NetworkCapabilities(); + capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, false); + capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, !isRoaming); + for (int nc : oemNetCapabilities) { + capabilities.setCapability(nc, true); + } + capabilities.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR); + return new NetworkStateSnapshot(MOBILE_NETWORK, capabilities, prop, subscriberId, + TYPE_MOBILE); + } + + private static NetworkStateSnapshot buildImsState( + String subscriberId, int subId, String ifaceName) { + final LinkProperties prop = new LinkProperties(); + prop.setInterfaceName(ifaceName); + final NetworkCapabilities capabilities = new NetworkCapabilities(); + capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, true); + capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, true); + capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_IMS, true); + capabilities.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR); + capabilities.setNetworkSpecifier(new TelephonyNetworkSpecifier(subId)); + return new NetworkStateSnapshot( + MOBILE_NETWORK, capabilities, prop, subscriberId, TYPE_MOBILE); + } + + private long getElapsedRealtime() { + return mElapsedRealtime; + } + + private long startTimeMillis() { + return TEST_START; + } + + private long currentTimeMillis() { + return startTimeMillis() + mElapsedRealtime; + } + + private void incrementCurrentTime(long duration) { + mElapsedRealtime += duration; + } + + private void forcePollAndWaitForIdle() { + mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL)); + waitForIdle(); + } + + private void waitForIdle() { + HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT); + } + + static class LatchedHandler extends Handler { + private final ConditionVariable mCv; + int lastMessageType = INVALID_TYPE; + + LatchedHandler(Looper looper, ConditionVariable cv) { + super(looper); + mCv = cv; + } + + @Override + public void handleMessage(Message msg) { + lastMessageType = msg.what; + mCv.open(); + super.handleMessage(msg); + } + } +} diff --git a/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java b/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java new file mode 100644 index 0000000000..6d2c7dc39f --- /dev/null +++ b/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java @@ -0,0 +1,386 @@ +/* + * Copyright (C) 2020 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.net; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +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.content.Context; +import android.net.NetworkTemplate; +import android.os.test.TestLooper; +import android.telephony.NetworkRegistrationInfo; +import android.telephony.PhoneStateListener; +import android.telephony.ServiceState; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import com.android.internal.util.CollectionUtils; +import com.android.server.net.NetworkStatsSubscriptionsMonitor.RatTypeListener; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +@RunWith(JUnit4.class) +public final class NetworkStatsSubscriptionsMonitorTest { + private static final int TEST_SUBID1 = 3; + private static final int TEST_SUBID2 = 5; + private static final String TEST_IMSI1 = "466921234567890"; + private static final String TEST_IMSI2 = "466920987654321"; + private static final String TEST_IMSI3 = "466929999999999"; + + @Mock private Context mContext; + @Mock private SubscriptionManager mSubscriptionManager; + @Mock private TelephonyManager mTelephonyManager; + @Mock private NetworkStatsSubscriptionsMonitor.Delegate mDelegate; + private final List mTestSubList = new ArrayList<>(); + + private final Executor mExecutor = Executors.newSingleThreadExecutor(); + private NetworkStatsSubscriptionsMonitor mMonitor; + private TestLooper mTestLooper = new TestLooper(); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + when(mTelephonyManager.createForSubscriptionId(anyInt())).thenReturn(mTelephonyManager); + + when(mContext.getSystemService(eq(Context.TELEPHONY_SUBSCRIPTION_SERVICE))) + .thenReturn(mSubscriptionManager); + when(mContext.getSystemService(eq(Context.TELEPHONY_SERVICE))) + .thenReturn(mTelephonyManager); + + mMonitor = new NetworkStatsSubscriptionsMonitor(mContext, mTestLooper.getLooper(), + mExecutor, mDelegate); + } + + @Test + public void testStartStop() { + // Verify that addOnSubscriptionsChangedListener() is never called before start(). + verify(mSubscriptionManager, never()) + .addOnSubscriptionsChangedListener(mExecutor, mMonitor); + mMonitor.start(); + verify(mSubscriptionManager).addOnSubscriptionsChangedListener(mExecutor, mMonitor); + + // Verify that removeOnSubscriptionsChangedListener() is never called before stop() + verify(mSubscriptionManager, never()).removeOnSubscriptionsChangedListener(mMonitor); + mMonitor.stop(); + verify(mSubscriptionManager).removeOnSubscriptionsChangedListener(mMonitor); + } + + @NonNull + private static int[] convertArrayListToIntArray(@NonNull List arrayList) { + final int[] list = new int[arrayList.size()]; + for (int i = 0; i < arrayList.size(); i++) { + list[i] = arrayList.get(i); + } + return list; + } + + private void setRatTypeForSub(List listeners, + int subId, int type) { + final ServiceState serviceState = mock(ServiceState.class); + when(serviceState.getDataNetworkType()).thenReturn(type); + final RatTypeListener match = CollectionUtils + .find(listeners, it -> it.getSubId() == subId); + if (match == null) { + fail("Could not find listener with subId: " + subId); + } + match.onServiceStateChanged(serviceState); + } + + private void addTestSub(int subId, String subscriberId) { + // add SubId to TestSubList. + if (mTestSubList.contains(subId)) fail("The subscriber list already contains this ID"); + + mTestSubList.add(subId); + + final int[] subList = convertArrayListToIntArray(mTestSubList); + when(mSubscriptionManager.getCompleteActiveSubscriptionIdList()).thenReturn(subList); + when(mTelephonyManager.getSubscriberId(subId)).thenReturn(subscriberId); + mMonitor.onSubscriptionsChanged(); + } + + private void updateSubscriberIdForTestSub(int subId, @Nullable final String subscriberId) { + when(mTelephonyManager.getSubscriberId(subId)).thenReturn(subscriberId); + mMonitor.onSubscriptionsChanged(); + } + + private void removeTestSub(int subId) { + // Remove subId from TestSubList. + mTestSubList.removeIf(it -> it == subId); + final int[] subList = convertArrayListToIntArray(mTestSubList); + when(mSubscriptionManager.getCompleteActiveSubscriptionIdList()).thenReturn(subList); + mMonitor.onSubscriptionsChanged(); + } + + private void assertRatTypeChangedForSub(String subscriberId, int ratType) { + assertEquals(ratType, mMonitor.getRatTypeForSubscriberId(subscriberId)); + final ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(Integer.class); + // Verify callback with the subscriberId and the RAT type should be as expected. + // It will fail if get a callback with an unexpected RAT type. + verify(mDelegate).onCollapsedRatTypeChanged(eq(subscriberId), typeCaptor.capture()); + final int type = typeCaptor.getValue(); + assertEquals(ratType, type); + } + + private void assertRatTypeNotChangedForSub(String subscriberId, int ratType) { + assertEquals(mMonitor.getRatTypeForSubscriberId(subscriberId), ratType); + // Should never get callback with any RAT type. + verify(mDelegate, never()).onCollapsedRatTypeChanged(eq(subscriberId), anyInt()); + } + + @Test + public void testSubChangedAndRatTypeChanged() { + final ArgumentCaptor ratTypeListenerCaptor = + ArgumentCaptor.forClass(RatTypeListener.class); + + mMonitor.start(); + // Insert sim1, verify RAT type is NETWORK_TYPE_UNKNOWN, and never get any callback + // before changing RAT type. + addTestSub(TEST_SUBID1, TEST_IMSI1); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + + // Insert sim2. + addTestSub(TEST_SUBID2, TEST_IMSI2); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + verify(mTelephonyManager, times(2)).listen(ratTypeListenerCaptor.capture(), + eq(PhoneStateListener.LISTEN_SERVICE_STATE)); + reset(mDelegate); + + // Set RAT type of sim1 to UMTS. + // Verify RAT type of sim1 after subscription gets onCollapsedRatTypeChanged() callback + // and others remain untouched. + setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID1, + TelephonyManager.NETWORK_TYPE_UMTS); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS); + assertRatTypeNotChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_UNKNOWN); + assertRatTypeNotChangedForSub(TEST_IMSI3, TelephonyManager.NETWORK_TYPE_UNKNOWN); + reset(mDelegate); + + // Set RAT type of sim2 to LTE. + // Verify RAT type of sim2 after subscription gets onCollapsedRatTypeChanged() callback + // and others remain untouched. + setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID2, + TelephonyManager.NETWORK_TYPE_LTE); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS); + assertRatTypeChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_LTE); + assertRatTypeNotChangedForSub(TEST_IMSI3, TelephonyManager.NETWORK_TYPE_UNKNOWN); + reset(mDelegate); + + // Remove sim2 and verify that callbacks are fired and RAT type is correct for sim2. + // while the other two remain untouched. + removeTestSub(TEST_SUBID2); + verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_NONE)); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS); + assertRatTypeChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_UNKNOWN); + assertRatTypeNotChangedForSub(TEST_IMSI3, TelephonyManager.NETWORK_TYPE_UNKNOWN); + reset(mDelegate); + + // Set RAT type of sim1 to UNKNOWN. Then stop monitoring subscription changes + // and verify that the listener for sim1 is removed. + setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID1, + TelephonyManager.NETWORK_TYPE_UNKNOWN); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + reset(mDelegate); + + mMonitor.stop(); + verify(mTelephonyManager, times(2)).listen(any(), eq(PhoneStateListener.LISTEN_NONE)); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + } + + + @Test + public void test5g() { + mMonitor.start(); + // Insert sim1, verify RAT type is NETWORK_TYPE_UNKNOWN, and never get any callback + // before changing RAT type. Also capture listener for later use. + addTestSub(TEST_SUBID1, TEST_IMSI1); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + final ArgumentCaptor ratTypeListenerCaptor = + ArgumentCaptor.forClass(RatTypeListener.class); + verify(mTelephonyManager, times(1)).listen(ratTypeListenerCaptor.capture(), + eq(PhoneStateListener.LISTEN_SERVICE_STATE)); + final RatTypeListener listener = CollectionUtils + .find(ratTypeListenerCaptor.getAllValues(), it -> it.getSubId() == TEST_SUBID1); + assertNotNull(listener); + + // Set RAT type to 5G NSA (non-standalone) mode, verify the monitor outputs + // NETWORK_TYPE_5G_NSA. + final ServiceState serviceState = mock(ServiceState.class); + when(serviceState.getDataNetworkType()).thenReturn(TelephonyManager.NETWORK_TYPE_LTE); + when(serviceState.getNrState()).thenReturn(NetworkRegistrationInfo.NR_STATE_CONNECTED); + listener.onServiceStateChanged(serviceState); + assertRatTypeChangedForSub(TEST_IMSI1, NetworkTemplate.NETWORK_TYPE_5G_NSA); + reset(mDelegate); + + // Set RAT type to LTE without NR connected, the RAT type should be downgraded to LTE. + when(serviceState.getNrState()).thenReturn(NetworkRegistrationInfo.NR_STATE_NONE); + listener.onServiceStateChanged(serviceState); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_LTE); + reset(mDelegate); + + // Verify NR connected with other RAT type does not take effect. + when(serviceState.getDataNetworkType()).thenReturn(TelephonyManager.NETWORK_TYPE_UMTS); + when(serviceState.getNrState()).thenReturn(NetworkRegistrationInfo.NR_STATE_CONNECTED); + listener.onServiceStateChanged(serviceState); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS); + reset(mDelegate); + + // Set RAT type to 5G standalone mode, the RAT type should be NR. + setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID1, + TelephonyManager.NETWORK_TYPE_NR); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_NR); + reset(mDelegate); + + // Set NR state to none in standalone mode does not change anything. + when(serviceState.getDataNetworkType()).thenReturn(TelephonyManager.NETWORK_TYPE_NR); + when(serviceState.getNrState()).thenReturn(NetworkRegistrationInfo.NR_STATE_NONE); + listener.onServiceStateChanged(serviceState); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_NR); + } + + @Test + public void testSubscriberIdUnavailable() { + final ArgumentCaptor ratTypeListenerCaptor = + ArgumentCaptor.forClass(RatTypeListener.class); + + mMonitor.start(); + // Insert sim1, set subscriberId to null which is normal in SIM PIN locked case. + // Verify RAT type is NETWORK_TYPE_UNKNOWN and service will not perform listener + // registration. + addTestSub(TEST_SUBID1, null); + verify(mTelephonyManager, never()).listen(any(), anyInt()); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + + // Set IMSI for sim1, verify the listener will be registered. + updateSubscriberIdForTestSub(TEST_SUBID1, TEST_IMSI1); + verify(mTelephonyManager, times(1)).listen(ratTypeListenerCaptor.capture(), + eq(PhoneStateListener.LISTEN_SERVICE_STATE)); + reset(mTelephonyManager); + when(mTelephonyManager.createForSubscriptionId(anyInt())).thenReturn(mTelephonyManager); + + // Set RAT type of sim1 to UMTS. Verify RAT type of sim1 is changed. + setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID1, + TelephonyManager.NETWORK_TYPE_UMTS); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS); + reset(mDelegate); + + // Set IMSI to null again to simulate somehow IMSI is not available, such as + // modem crash. Verify service should unregister listener. + updateSubscriberIdForTestSub(TEST_SUBID1, null); + verify(mTelephonyManager, times(1)).listen(eq(ratTypeListenerCaptor.getValue()), + eq(PhoneStateListener.LISTEN_NONE)); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + reset(mDelegate); + clearInvocations(mTelephonyManager); + + // Simulate somehow IMSI is back. Verify service will register with + // another listener and fire callback accordingly. + final ArgumentCaptor ratTypeListenerCaptor2 = + ArgumentCaptor.forClass(RatTypeListener.class); + updateSubscriberIdForTestSub(TEST_SUBID1, TEST_IMSI1); + verify(mTelephonyManager, times(1)).listen(ratTypeListenerCaptor2.capture(), + eq(PhoneStateListener.LISTEN_SERVICE_STATE)); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + reset(mDelegate); + clearInvocations(mTelephonyManager); + + // Set RAT type of sim1 to LTE. Verify RAT type of sim1 still works. + setRatTypeForSub(ratTypeListenerCaptor2.getAllValues(), TEST_SUBID1, + TelephonyManager.NETWORK_TYPE_LTE); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_LTE); + reset(mDelegate); + + mMonitor.stop(); + verify(mTelephonyManager, times(1)).listen(eq(ratTypeListenerCaptor2.getValue()), + eq(PhoneStateListener.LISTEN_NONE)); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + } + + /** + * Verify that when IMSI suddenly changed for a given subId, the service will register a new + * listener and unregister the old one, and report changes on updated IMSI. This is for modem + * feature that may be enabled for certain carrier, which changes to use a different IMSI while + * roaming on certain networks for multi-IMSI SIM cards, but the subId stays the same. + */ + @Test + public void testSubscriberIdChanged() { + mMonitor.start(); + // Insert sim1, verify RAT type is NETWORK_TYPE_UNKNOWN, and never get any callback + // before changing RAT type. + addTestSub(TEST_SUBID1, TEST_IMSI1); + final ArgumentCaptor ratTypeListenerCaptor = + ArgumentCaptor.forClass(RatTypeListener.class); + verify(mTelephonyManager, times(1)).listen(ratTypeListenerCaptor.capture(), + eq(PhoneStateListener.LISTEN_SERVICE_STATE)); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + + // Set RAT type of sim1 to UMTS. + // Verify RAT type of sim1 changes accordingly. + setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID1, + TelephonyManager.NETWORK_TYPE_UMTS); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS); + reset(mDelegate); + clearInvocations(mTelephonyManager); + + // Simulate IMSI of sim1 changed to IMSI2. Verify the service will register with + // another listener and remove the old one. The RAT type of new IMSI stays at + // NETWORK_TYPE_UNKNOWN until received initial callback from telephony. + final ArgumentCaptor ratTypeListenerCaptor2 = + ArgumentCaptor.forClass(RatTypeListener.class); + updateSubscriberIdForTestSub(TEST_SUBID1, TEST_IMSI2); + verify(mTelephonyManager, times(1)).listen(ratTypeListenerCaptor2.capture(), + eq(PhoneStateListener.LISTEN_SERVICE_STATE)); + verify(mTelephonyManager, times(1)).listen(eq(ratTypeListenerCaptor.getValue()), + eq(PhoneStateListener.LISTEN_NONE)); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + assertRatTypeNotChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_UNKNOWN); + reset(mDelegate); + + // Set RAT type of sim1 to UMTS for new listener to simulate the initial callback received + // from telephony after registration. Verify RAT type of sim1 changes with IMSI2 + // accordingly. + setRatTypeForSub(ratTypeListenerCaptor2.getAllValues(), TEST_SUBID1, + TelephonyManager.NETWORK_TYPE_UMTS); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + assertRatTypeChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_UMTS); + reset(mDelegate); + } +} diff --git a/tests/unit/java/com/android/server/net/ipmemorystore/NetworkAttributesTest.java b/tests/unit/java/com/android/server/net/ipmemorystore/NetworkAttributesTest.java new file mode 100644 index 0000000000..ebbc0ef625 --- /dev/null +++ b/tests/unit/java/com/android/server/net/ipmemorystore/NetworkAttributesTest.java @@ -0,0 +1,70 @@ +/* + * 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 com.android.server.net.ipmemorystore; + +import static org.junit.Assert.assertEquals; + +import android.net.ipmemorystore.NetworkAttributes; +import android.net.networkstack.aidl.quirks.IPv6ProvisioningLossQuirk; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.lang.reflect.Field; +import java.net.Inet4Address; +import java.net.UnknownHostException; +import java.util.Arrays; + +/** Unit tests for {@link NetworkAttributes}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class NetworkAttributesTest { + private static final String WEIGHT_FIELD_NAME_PREFIX = "WEIGHT_"; + private static final float EPSILON = 0.0001f; + + // This is running two tests to make sure the total weight is the sum of all weights. To be + // sure this is not fireproof, but you'd kind of need to do it on purpose to pass. + @Test + public void testTotalWeight() throws IllegalAccessException, UnknownHostException { + // Make sure that TOTAL_WEIGHT is equal to the sum of the fields starting with WEIGHT_ + float sum = 0f; + final Field[] fieldList = NetworkAttributes.class.getDeclaredFields(); + for (final Field field : fieldList) { + if (!field.getName().startsWith(WEIGHT_FIELD_NAME_PREFIX)) continue; + field.setAccessible(true); + sum += (float) field.get(null); + } + assertEquals(sum, NetworkAttributes.TOTAL_WEIGHT, EPSILON); + + final IPv6ProvisioningLossQuirk ipv6ProvisioningLossQuirk = + new IPv6ProvisioningLossQuirk(3, System.currentTimeMillis() + 7_200_000); + // Use directly the constructor with all attributes, and make sure that when compared + // to itself the score is a clean 1.0f. + final NetworkAttributes na = + new NetworkAttributes( + (Inet4Address) Inet4Address.getByAddress(new byte[] {1, 2, 3, 4}), + System.currentTimeMillis() + 7_200_000, + "some hint", + Arrays.asList(Inet4Address.getByAddress(new byte[] {5, 6, 7, 8}), + Inet4Address.getByAddress(new byte[] {9, 0, 1, 2})), + 98, ipv6ProvisioningLossQuirk); + assertEquals(1.0f, na.getNetworkGroupSamenessConfidence(na), EPSILON); + } +} diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp new file mode 100644 index 0000000000..22a04f5c09 --- /dev/null +++ b/tests/unit/jni/Android.bp @@ -0,0 +1,32 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +cc_library_shared { + name: "libnetworkstatsfactorytestjni", + + cflags: [ + "-Wall", + "-Werror", + "-Wno-unused-parameter", + "-Wthread-safety", + ], + + srcs: [ + ":lib_networkStatsFactory_native", + "test_onload.cpp", + ], + + shared_libs: [ + "libbpf_android", + "liblog", + "libnativehelper", + "libnetdbpf", + "libnetdutils", + ], +} diff --git a/tests/unit/jni/test_onload.cpp b/tests/unit/jni/test_onload.cpp new file mode 100644 index 0000000000..5194ddb0d8 --- /dev/null +++ b/tests/unit/jni/test_onload.cpp @@ -0,0 +1,44 @@ +/* + * 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. + */ + +/* + * this is a mini native libaray for NetworkStatsFactoryTest to run properly. It + * load all the native method related to NetworkStatsFactory when test run + */ +#include +#include "jni.h" +#include "utils/Log.h" +#include "utils/misc.h" + +namespace android { +int register_android_server_net_NetworkStatsFactory(JNIEnv* env); +}; + +using namespace android; + +extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) +{ + JNIEnv* env = NULL; + jint result = -1; + + if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { + ALOGE("GetEnv failed!"); + return result; + } + ALOG_ASSERT(env, "Could not retrieve the env!"); + register_android_server_net_NetworkStatsFactory(env); + return JNI_VERSION_1_4; +} diff --git a/tests/unit/res/raw/history_v1 b/tests/unit/res/raw/history_v1 new file mode 100644 index 0000000000..de79491c03 Binary files /dev/null and b/tests/unit/res/raw/history_v1 differ diff --git a/tests/unit/res/raw/net_dev_typical b/tests/unit/res/raw/net_dev_typical new file mode 100644 index 0000000000..290bf03eb9 --- /dev/null +++ b/tests/unit/res/raw/net_dev_typical @@ -0,0 +1,8 @@ +Inter-| Receive | Transmit + face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed + lo: 8308 116 0 0 0 0 0 0 8308 116 0 0 0 0 0 0 +rmnet0: 1507570 2205 0 0 0 0 0 0 489339 2237 0 0 0 0 0 0 + ifb0: 52454 151 0 151 0 0 0 0 0 0 0 0 0 0 0 0 + ifb1: 52454 151 0 151 0 0 0 0 0 0 0 0 0 0 0 0 + sit0: 0 0 0 0 0 0 0 0 0 0 148 0 0 0 0 0 +ip6tnl0: 0 0 0 0 0 0 0 0 0 0 151 151 0 0 0 0 diff --git a/tests/unit/res/raw/netstats_uid_v4 b/tests/unit/res/raw/netstats_uid_v4 new file mode 100644 index 0000000000..e75fc1ca5c Binary files /dev/null and b/tests/unit/res/raw/netstats_uid_v4 differ diff --git a/tests/unit/res/raw/netstats_v1 b/tests/unit/res/raw/netstats_v1 new file mode 100644 index 0000000000..e80860a6b9 Binary files /dev/null and b/tests/unit/res/raw/netstats_v1 differ diff --git a/tests/unit/res/raw/xt_qtaguid_iface_fmt_typical b/tests/unit/res/raw/xt_qtaguid_iface_fmt_typical new file mode 100644 index 0000000000..656d5bb82d --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_iface_fmt_typical @@ -0,0 +1,4 @@ +ifname total_skb_rx_bytes total_skb_rx_packets total_skb_tx_bytes total_skb_tx_packets +rmnet2 4968 35 3081 39 +rmnet1 11153922 8051 190226 2468 +rmnet0 6824 16 5692 10 diff --git a/tests/unit/res/raw/xt_qtaguid_iface_typical b/tests/unit/res/raw/xt_qtaguid_iface_typical new file mode 100644 index 0000000000..610723aef2 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_iface_typical @@ -0,0 +1,6 @@ +rmnet3 1 0 0 0 0 20822 501 1149991 815 +rmnet2 1 0 0 0 0 1594 15 1313 15 +rmnet1 1 0 0 0 0 207398 458 166918 565 +rmnet0 1 0 0 0 0 2112 24 700 10 +test1 1 1 2 3 4 5 6 7 8 +test2 0 1 2 3 4 5 6 7 8 diff --git a/tests/unit/res/raw/xt_qtaguid_typical b/tests/unit/res/raw/xt_qtaguid_typical new file mode 100644 index 0000000000..c1b0d25995 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_typical @@ -0,0 +1,71 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 wlan0 0x0 0 0 18621 96 2898 44 312 6 15897 58 2412 32 312 6 1010 16 1576 22 +3 wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +4 wlan0 0x0 1000 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +5 wlan0 0x0 1000 1 1949 13 1078 14 0 0 1600 10 349 3 0 0 600 10 478 4 +6 wlan0 0x0 10005 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +7 wlan0 0x0 10005 1 32081 38 5315 50 32081 38 0 0 0 0 5315 50 0 0 0 0 +8 wlan0 0x0 10011 0 35777 53 5718 57 0 0 0 0 35777 53 0 0 0 0 5718 57 +9 wlan0 0x0 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +10 wlan0 0x0 10014 0 0 0 1098 13 0 0 0 0 0 0 0 0 0 0 1098 13 +11 wlan0 0x0 10014 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +12 wlan0 0x0 10021 0 562386 573 49228 549 0 0 0 0 562386 573 0 0 0 0 49228 549 +13 wlan0 0x0 10021 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +14 wlan0 0x0 10031 0 3425 5 586 6 0 0 0 0 3425 5 0 0 0 0 586 6 +15 wlan0 0x0 10031 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +16 wlan0 0x7fffff0100000000 10021 0 562386 573 49228 549 0 0 0 0 562386 573 0 0 0 0 49228 549 +17 wlan0 0x7fffff0100000000 10021 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +18 wlan0 0x7fffff0100000000 10031 0 3425 5 586 6 0 0 0 0 3425 5 0 0 0 0 586 6 +19 wlan0 0x7fffff0100000000 10031 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +20 rmnet2 0x0 0 0 547 5 118 2 40 1 243 1 264 3 0 0 62 1 56 1 +21 rmnet2 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +22 rmnet2 0x0 10001 0 1125899906842624 5 984 11 632 5 0 0 0 0 984 11 0 0 0 0 +23 rmnet2 0x0 10001 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +24 rmnet1 0x0 0 0 26736 174 7098 130 7210 97 18382 64 1144 13 2932 64 4054 64 112 2 +25 rmnet1 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +26 rmnet1 0x0 1000 0 75774 77 18038 78 75335 72 439 5 0 0 17668 73 370 5 0 0 +27 rmnet1 0x0 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +28 rmnet1 0x0 10007 0 269945 578 111632 586 269945 578 0 0 0 0 111632 586 0 0 0 0 +29 rmnet1 0x0 10007 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +30 rmnet1 0x0 10011 0 1741256 6918 769778 7019 1741256 6918 0 0 0 0 769778 7019 0 0 0 0 +31 rmnet1 0x0 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +32 rmnet1 0x0 10014 0 0 0 786 12 0 0 0 0 0 0 786 12 0 0 0 0 +33 rmnet1 0x0 10014 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +34 rmnet1 0x0 10021 0 433533 1454 393420 1604 433533 1454 0 0 0 0 393420 1604 0 0 0 0 +35 rmnet1 0x0 10021 1 21215 33 10278 33 21215 33 0 0 0 0 10278 33 0 0 0 0 +36 rmnet1 0x0 10036 0 6310 25 3284 29 6310 25 0 0 0 0 3284 29 0 0 0 0 +37 rmnet1 0x0 10036 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +38 rmnet1 0x0 10047 0 34264 47 3936 34 34264 47 0 0 0 0 3936 34 0 0 0 0 +39 rmnet1 0x0 10047 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +40 rmnet1 0x4e7700000000 10011 0 9187 27 4248 33 9187 27 0 0 0 0 4248 33 0 0 0 0 +41 rmnet1 0x4e7700000000 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +42 rmnet1 0x1000000000000000 10007 0 2109 4 791 4 2109 4 0 0 0 0 791 4 0 0 0 0 +43 rmnet1 0x1000000000000000 10007 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +44 rmnet1 0x1000000400000000 10007 0 9811 22 6286 22 9811 22 0 0 0 0 6286 22 0 0 0 0 +45 rmnet1 0x1000000400000000 10007 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +46 rmnet1 0x1010000000000000 10021 0 164833 426 135392 527 164833 426 0 0 0 0 135392 527 0 0 0 0 +47 rmnet1 0x1010000000000000 10021 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +48 rmnet1 0x1144000400000000 10011 0 10112 18 3334 17 10112 18 0 0 0 0 3334 17 0 0 0 0 +49 rmnet1 0x1144000400000000 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +50 rmnet1 0x1244000400000000 10011 0 1300 3 848 2 1300 3 0 0 0 0 848 2 0 0 0 0 +51 rmnet1 0x1244000400000000 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +52 rmnet1 0x3000000000000000 10007 0 10389 14 1521 12 10389 14 0 0 0 0 1521 12 0 0 0 0 +53 rmnet1 0x3000000000000000 10007 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +54 rmnet1 0x3000000400000000 10007 0 238070 380 93938 404 238070 380 0 0 0 0 93938 404 0 0 0 0 +55 rmnet1 0x3000000400000000 10007 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +56 rmnet1 0x3010000000000000 10021 0 219110 578 227423 676 219110 578 0 0 0 0 227423 676 0 0 0 0 +57 rmnet1 0x3010000000000000 10021 1 742 3 1265 3 742 3 0 0 0 0 1265 3 0 0 0 0 +58 rmnet1 0x3020000000000000 10021 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +59 rmnet1 0x3020000000000000 10021 1 20473 30 9013 30 20473 30 0 0 0 0 9013 30 0 0 0 0 +60 rmnet1 0x3144000400000000 10011 0 43963 92 34414 116 43963 92 0 0 0 0 34414 116 0 0 0 0 +61 rmnet1 0x3144000400000000 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +62 rmnet1 0x3244000400000000 10011 0 3486 8 1520 9 3486 8 0 0 0 0 1520 9 0 0 0 0 +63 rmnet1 0x3244000400000000 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +64 rmnet1 0x7fffff0100000000 10021 0 29102 56 8865 60 29102 56 0 0 0 0 8865 60 0 0 0 0 +65 rmnet1 0x7fffff0100000000 10021 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +66 rmnet1 0x7fffff0300000000 1000 0 995 13 14145 14 995 13 0 0 0 0 14145 14 0 0 0 0 +67 rmnet1 0x7fffff0300000000 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +68 rmnet0 0x0 0 0 4312 49 1288 23 0 0 0 0 4312 49 0 0 0 0 1288 23 +69 rmnet0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +70 rmnet0 0x0 10080 0 22266 30 20976 30 0 0 0 0 22266 30 0 0 0 0 20976 30 +71 rmnet0 0x0 10080 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_incorrect_iface b/tests/unit/res/raw/xt_qtaguid_vpn_incorrect_iface new file mode 100644 index 0000000000..fc92715253 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_vpn_incorrect_iface @@ -0,0 +1,3 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 test_nss_tun0 0x0 1001 0 1000 100 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +3 test1 0x0 1004 0 1100 100 1100 100 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying new file mode 100644 index 0000000000..1ef18894b6 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying @@ -0,0 +1,5 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 test_nss_tun0 0x0 1001 0 2000 200 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +3 test_nss_tun0 0x0 1002 0 1000 100 500 50 0 0 0 0 0 0 0 0 0 0 0 0 +4 test0 0x0 1004 0 3300 300 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +5 test0 0x0 1004 1 0 0 1650 150 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_compression b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_compression new file mode 100644 index 0000000000..6d6bf550bb --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_compression @@ -0,0 +1,4 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 test_nss_tun0 0x0 1001 0 1000 100 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +3 test_nss_tun0 0x0 1002 0 3000 300 3000 300 0 0 0 0 0 0 0 0 0 0 0 0 +4 test0 0x0 1004 0 1000 100 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_own_traffic b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_own_traffic new file mode 100644 index 0000000000..2c2e5d2555 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_own_traffic @@ -0,0 +1,6 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 test_nss_tun0 0x0 1001 0 2000 200 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +3 test_nss_tun0 0x0 1002 0 1000 100 500 50 0 0 0 0 0 0 0 0 0 0 0 0 +4 test_nss_tun0 0x0 1004 0 5000 500 6000 600 0 0 0 0 0 0 0 0 0 0 0 0 +5 test0 0x0 1004 0 8800 800 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +6 test0 0x0 1004 1 0 0 8250 750 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_two_vpn b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_two_vpn new file mode 100644 index 0000000000..eb0513b100 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_two_vpn @@ -0,0 +1,9 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 test_nss_tun0 0x0 1001 0 2000 200 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +3 test_nss_tun0 0x0 1002 0 1000 100 500 50 0 0 0 0 0 0 0 0 0 0 0 0 +4 test_nss_tun1 0x0 1001 0 3000 300 700 70 0 0 0 0 0 0 0 0 0 0 0 0 +5 test_nss_tun1 0x0 1002 0 500 50 250 25 0 0 0 0 0 0 0 0 0 0 0 0 +6 test0 0x0 1004 0 3300 300 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +7 test0 0x0 1004 1 0 0 1650 150 0 0 0 0 0 0 0 0 0 0 0 0 +8 test1 0x0 1004 0 3850 350 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +9 test1 0x0 1004 1 0 0 1045 95 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_rewrite_through_self b/tests/unit/res/raw/xt_qtaguid_vpn_rewrite_through_self new file mode 100644 index 0000000000..afcdd71990 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_vpn_rewrite_through_self @@ -0,0 +1,6 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 test_nss_tun0 0x0 1001 0 2000 200 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +3 test_nss_tun0 0x0 1002 0 1000 100 500 50 0 0 0 0 0 0 0 0 0 0 0 0 +4 test_nss_tun0 0x0 1004 0 0 0 1600 160 0 0 0 0 0 0 0 0 0 0 0 0 +5 test0 0x0 1004 1 0 0 1760 176 0 0 0 0 0 0 0 0 0 0 0 0 +6 test0 0x0 1004 0 3300 300 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_duplication b/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_duplication new file mode 100644 index 0000000000..d7c7eb9f4a --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_duplication @@ -0,0 +1,5 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 test_nss_tun0 0x0 1001 0 1000 100 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +3 test_nss_tun0 0x0 1002 0 1000 100 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +4 test0 0x0 1004 0 2200 200 2200 200 0 0 0 0 0 0 0 0 0 0 0 0 +5 test1 0x0 1004 0 2200 200 2200 200 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_split b/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_split new file mode 100644 index 0000000000..38a3dce4a8 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_split @@ -0,0 +1,4 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 test_nss_tun0 0x0 1001 0 500 50 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +3 test0 0x0 1004 0 330 30 660 60 0 0 0 0 0 0 0 0 0 0 0 0 +4 test1 0x0 1004 0 220 20 440 40 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_split_compression b/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_split_compression new file mode 100644 index 0000000000..d35244b3b4 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_split_compression @@ -0,0 +1,4 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 test_nss_tun0 0x0 1001 0 1000 100 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +3 test0 0x0 1004 0 600 60 600 60 0 0 0 0 0 0 0 0 0 0 0 0 +4 test1 0x0 1004 0 200 20 200 20 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_with_clat b/tests/unit/res/raw/xt_qtaguid_vpn_with_clat new file mode 100644 index 0000000000..0d893d515c --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_vpn_with_clat @@ -0,0 +1,8 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 test_nss_tun0 0x0 1001 0 2000 200 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +3 test_nss_tun0 0x0 1002 0 1000 100 500 50 0 0 0 0 0 0 0 0 0 0 0 0 +4 v4-test0 0x0 1004 0 3300 300 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +5 v4-test0 0x0 1004 1 0 0 1650 150 0 0 0 0 0 0 0 0 0 0 0 0 +6 test0 0x0 0 0 9300 300 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +7 test0 0x0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +8 test0 0x0 1029 0 0 0 4650 150 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/tests/unit/res/raw/xt_qtaguid_with_clat b/tests/unit/res/raw/xt_qtaguid_with_clat new file mode 100644 index 0000000000..f04b32f083 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_with_clat @@ -0,0 +1,43 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 v4-wlan0 0x0 0 0 256 5 196 4 256 5 0 0 0 0 196 4 0 0 0 0 +3 v4-wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +4 v4-wlan0 0x0 1000 0 30312 25 1770 27 30236 24 76 1 0 0 1694 26 76 1 0 0 +5 v4-wlan0 0x0 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +6 v4-wlan0 0x0 10060 0 9398432 6717 169412 4235 9398432 6717 0 0 0 0 169412 4235 0 0 0 0 +7 v4-wlan0 0x0 10060 1 1448660 1041 31192 753 1448660 1041 0 0 0 0 31192 753 0 0 0 0 +8 v4-wlan0 0x0 10102 0 9702 16 2870 23 9702 16 0 0 0 0 2870 23 0 0 0 0 +9 v4-wlan0 0x0 10102 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +10 wlan0 0x0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +11 wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +12 wlan0 0x0 1000 0 6126 13 2013 16 5934 11 192 2 0 0 1821 14 192 2 0 0 +13 wlan0 0x0 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +14 wlan0 0x0 10013 0 0 0 144 2 0 0 0 0 0 0 144 2 0 0 0 0 +15 wlan0 0x0 10013 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +16 wlan0 0x0 10018 0 5980263 4715 167667 1922 5972583 4709 0 0 7680 6 167667 1922 0 0 0 0 +17 wlan0 0x0 10018 1 43995 37 2766 27 43995 37 0 0 0 0 2766 27 0 0 0 0 +18 wlan0 0x0 10060 0 134356 133 8705 74 134356 133 0 0 0 0 8705 74 0 0 0 0 +19 wlan0 0x0 10060 1 294709 326 26448 256 294709 326 0 0 0 0 26448 256 0 0 0 0 +20 wlan0 0x0 10079 0 10926 13 1507 13 10926 13 0 0 0 0 1507 13 0 0 0 0 +21 wlan0 0x0 10079 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +22 wlan0 0x0 10102 0 25038 42 8245 57 25038 42 0 0 0 0 8245 57 0 0 0 0 +23 wlan0 0x0 10102 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +24 wlan0 0x0 10103 0 0 0 192 2 0 0 0 0 0 0 0 0 192 2 0 0 +25 wlan0 0x0 10103 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +26 wlan0 0x1000040700000000 10018 0 831 6 655 5 831 6 0 0 0 0 655 5 0 0 0 0 +27 wlan0 0x1000040700000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +28 wlan0 0x1000040b00000000 10018 0 1714 8 1561 7 1714 8 0 0 0 0 1561 7 0 0 0 0 +29 wlan0 0x1000040b00000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +30 wlan0 0x1000120300000000 10018 0 8243 11 2234 12 8243 11 0 0 0 0 2234 12 0 0 0 0 +31 wlan0 0x1000120300000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +32 wlan0 0x1000180300000000 10018 0 56368 49 4790 39 56368 49 0 0 0 0 4790 39 0 0 0 0 +33 wlan0 0x1000180300000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +34 wlan0 0x1000300000000000 10018 0 9488 17 18813 25 1808 11 0 0 7680 6 18813 25 0 0 0 0 +35 wlan0 0x1000300000000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +36 wlan0 0x3000180400000000 10018 0 131262 103 7416 103 131262 103 0 0 0 0 7416 103 0 0 0 0 +37 wlan0 0x3000180400000000 10018 1 43995 37 2766 27 43995 37 0 0 0 0 2766 27 0 0 0 0 +38 wlan0 0xffffff0100000000 10018 0 5771986 4518 131190 1725 5771986 4518 0 0 0 0 131190 1725 0 0 0 0 +39 wlan0 0xffffff0100000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +40 dummy0 0x0 0 0 0 0 168 3 0 0 0 0 0 0 0 0 0 0 168 3 +41 dummy0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +42 lo 0x0 0 0 1288 16 1288 16 0 0 532 8 756 8 0 0 532 8 756 8 +43 lo 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/tests/unit/res/raw/xt_qtaguid_with_clat_100mb_download_after b/tests/unit/res/raw/xt_qtaguid_with_clat_100mb_download_after new file mode 100644 index 0000000000..12d98ca29f --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_with_clat_100mb_download_after @@ -0,0 +1,189 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 r_rmnet_data0 0x0 0 0 0 0 392 6 0 0 0 0 0 0 0 0 0 0 392 6 +3 r_rmnet_data0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +4 v4-wlan0 0x0 0 0 58952 2072 2888 65 264 6 0 0 58688 2066 132 3 0 0 2756 62 +5 v4-wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +6 v4-wlan0 0x0 10034 0 6192 11 1445 11 6192 11 0 0 0 0 1445 11 0 0 0 0 +7 v4-wlan0 0x0 10034 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +8 v4-wlan0 0x0 10057 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +9 v4-wlan0 0x0 10057 1 728 7 392 7 0 0 728 7 0 0 0 0 392 7 0 0 +10 v4-wlan0 0x0 10106 0 2232 18 2232 18 0 0 2232 18 0 0 0 0 2232 18 0 0 +11 v4-wlan0 0x0 10106 1 432952718 314238 5442288 121260 432950238 314218 2480 20 0 0 5433900 121029 8388 231 0 0 +12 wlan0 0x0 0 0 330187296 250652 0 0 329106990 236273 226202 1255 854104 13124 0 0 0 0 0 0 +13 wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +14 wlan0 0x0 1000 0 77113 272 56151 575 77113 272 0 0 0 0 19191 190 36960 385 0 0 +15 wlan0 0x0 1000 1 20227 80 8356 72 18539 74 1688 6 0 0 7562 66 794 6 0 0 +16 wlan0 0x0 10006 0 80755 92 9122 99 80755 92 0 0 0 0 9122 99 0 0 0 0 +17 wlan0 0x0 10006 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +18 wlan0 0x0 10015 0 4390 7 14824 252 4390 7 0 0 0 0 14824 252 0 0 0 0 +19 wlan0 0x0 10015 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +20 wlan0 0x0 10018 0 4928 11 1741 14 4928 11 0 0 0 0 1741 14 0 0 0 0 +21 wlan0 0x0 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +22 wlan0 0x0 10020 0 21163552 34395 2351650 15326 21162947 34390 605 5 0 0 2351045 15321 605 5 0 0 +23 wlan0 0x0 10020 1 13835740 12938 1548795 6365 13833754 12920 1986 18 0 0 1546809 6347 1986 18 0 0 +24 wlan0 0x0 10023 0 13405 40 5042 44 13405 40 0 0 0 0 5042 44 0 0 0 0 +25 wlan0 0x0 10023 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +26 wlan0 0x0 10034 0 436394741 342648 6237981 80442 436394741 342648 0 0 0 0 6237981 80442 0 0 0 0 +27 wlan0 0x0 10034 1 64860872 51297 1335539 15546 64860872 51297 0 0 0 0 1335539 15546 0 0 0 0 +28 wlan0 0x0 10044 0 17614444 14774 521004 5694 17329882 14432 284562 342 0 0 419974 5408 101030 286 0 0 +29 wlan0 0x0 10044 1 17701 33 3100 28 17701 33 0 0 0 0 3100 28 0 0 0 0 +30 wlan0 0x0 10057 0 12312074 9339 436098 5450 12248060 9263 64014 76 0 0 414224 5388 21874 62 0 0 +31 wlan0 0x0 10057 1 1332953195 954797 31849632 457698 1331933207 953569 1019988 1228 0 0 31702284 456899 147348 799 0 0 +32 wlan0 0x0 10060 0 32972 200 433705 380 32972 200 0 0 0 0 433705 380 0 0 0 0 +33 wlan0 0x0 10060 1 32106 66 37789 87 32106 66 0 0 0 0 37789 87 0 0 0 0 +34 wlan0 0x0 10061 0 7675 23 2509 22 7675 23 0 0 0 0 2509 22 0 0 0 0 +35 wlan0 0x0 10061 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +36 wlan0 0x0 10074 0 38355 82 10447 97 38355 82 0 0 0 0 10447 97 0 0 0 0 +37 wlan0 0x0 10074 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +38 wlan0 0x0 10078 0 49013 79 7167 69 49013 79 0 0 0 0 7167 69 0 0 0 0 +39 wlan0 0x0 10078 1 5872 8 1236 10 5872 8 0 0 0 0 1236 10 0 0 0 0 +40 wlan0 0x0 10082 0 8301 13 1981 15 8301 13 0 0 0 0 1981 15 0 0 0 0 +41 wlan0 0x0 10082 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +42 wlan0 0x0 10086 0 7001 14 1579 15 7001 14 0 0 0 0 1579 15 0 0 0 0 +43 wlan0 0x0 10086 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +44 wlan0 0x0 10090 0 24327795 20224 920502 14661 24327795 20224 0 0 0 0 920502 14661 0 0 0 0 +45 wlan0 0x0 10090 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +46 wlan0 0x0 10092 0 36849 78 12449 81 36849 78 0 0 0 0 12449 81 0 0 0 0 +47 wlan0 0x0 10092 1 60 1 103 1 60 1 0 0 0 0 103 1 0 0 0 0 +48 wlan0 0x0 10095 0 131962 223 37069 241 131962 223 0 0 0 0 37069 241 0 0 0 0 +49 wlan0 0x0 10095 1 12949 21 3930 21 12949 21 0 0 0 0 3930 21 0 0 0 0 +50 wlan0 0x0 10106 0 30899554 22679 632476 12296 30895334 22645 4220 34 0 0 628256 12262 4220 34 0 0 +51 wlan0 0x0 10106 1 88923475 64963 1606962 35612 88917201 64886 3586 29 2688 48 1602032 35535 4930 77 0 0 +52 wlan0 0x40700000000 10020 0 705732 10589 404428 5504 705732 10589 0 0 0 0 404428 5504 0 0 0 0 +53 wlan0 0x40700000000 10020 1 2376 36 1296 18 2376 36 0 0 0 0 1296 18 0 0 0 0 +54 wlan0 0x40800000000 10020 0 34624 146 122525 160 34624 146 0 0 0 0 122525 160 0 0 0 0 +55 wlan0 0x40800000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +56 wlan0 0x40b00000000 10020 0 22411 85 7364 57 22411 85 0 0 0 0 7364 57 0 0 0 0 +57 wlan0 0x40b00000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +58 wlan0 0x120300000000 10020 0 76641 241 32783 169 76641 241 0 0 0 0 32783 169 0 0 0 0 +59 wlan0 0x120300000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +60 wlan0 0x130100000000 10020 0 73101 287 23236 203 73101 287 0 0 0 0 23236 203 0 0 0 0 +61 wlan0 0x130100000000 10020 1 264 4 144 2 264 4 0 0 0 0 144 2 0 0 0 0 +62 wlan0 0x180300000000 10020 0 330648 399 24736 232 330648 399 0 0 0 0 24736 232 0 0 0 0 +63 wlan0 0x180300000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +64 wlan0 0x180400000000 10020 0 21865 59 5022 42 21865 59 0 0 0 0 5022 42 0 0 0 0 +65 wlan0 0x180400000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +66 wlan0 0x300000000000 10020 0 15984 65 26927 57 15984 65 0 0 0 0 26927 57 0 0 0 0 +67 wlan0 0x300000000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +68 wlan0 0x1065fff00000000 10020 0 131871 599 93783 445 131871 599 0 0 0 0 93783 445 0 0 0 0 +69 wlan0 0x1065fff00000000 10020 1 264 4 144 2 264 4 0 0 0 0 144 2 0 0 0 0 +70 wlan0 0x1b24f4600000000 10034 0 15445 42 23329 45 15445 42 0 0 0 0 23329 45 0 0 0 0 +71 wlan0 0x1b24f4600000000 10034 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +72 wlan0 0x1000010000000000 10020 0 5542 9 1364 10 5542 9 0 0 0 0 1364 10 0 0 0 0 +73 wlan0 0x1000010000000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +74 wlan0 0x1000040100000000 10020 0 47196 184 213319 257 47196 184 0 0 0 0 213319 257 0 0 0 0 +75 wlan0 0x1000040100000000 10020 1 60 1 103 1 60 1 0 0 0 0 103 1 0 0 0 0 +76 wlan0 0x1000040700000000 10020 0 11599 50 10786 47 11599 50 0 0 0 0 10786 47 0 0 0 0 +77 wlan0 0x1000040700000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +78 wlan0 0x1000040800000000 10020 0 21902 145 174139 166 21902 145 0 0 0 0 174139 166 0 0 0 0 +79 wlan0 0x1000040800000000 10020 1 8568 88 105743 90 8568 88 0 0 0 0 105743 90 0 0 0 0 +80 wlan0 0x1000100300000000 10020 0 55213 118 194551 199 55213 118 0 0 0 0 194551 199 0 0 0 0 +81 wlan0 0x1000100300000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +82 wlan0 0x1000120300000000 10020 0 50826 74 21153 70 50826 74 0 0 0 0 21153 70 0 0 0 0 +83 wlan0 0x1000120300000000 10020 1 72 1 175 2 72 1 0 0 0 0 175 2 0 0 0 0 +84 wlan0 0x1000180300000000 10020 0 744198 657 65437 592 744198 657 0 0 0 0 65437 592 0 0 0 0 +85 wlan0 0x1000180300000000 10020 1 144719 132 10989 108 144719 132 0 0 0 0 10989 108 0 0 0 0 +86 wlan0 0x1000180600000000 10020 0 4599 8 1928 10 4599 8 0 0 0 0 1928 10 0 0 0 0 +87 wlan0 0x1000180600000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +88 wlan0 0x1000250000000000 10020 0 57740 98 13076 88 57740 98 0 0 0 0 13076 88 0 0 0 0 +89 wlan0 0x1000250000000000 10020 1 328 3 414 4 207 2 121 1 0 0 293 3 121 1 0 0 +90 wlan0 0x1000300000000000 10020 0 7675 30 31331 32 7675 30 0 0 0 0 31331 32 0 0 0 0 +91 wlan0 0x1000300000000000 10020 1 30173 97 101335 100 30173 97 0 0 0 0 101335 100 0 0 0 0 +92 wlan0 0x1000310200000000 10020 0 1681 9 2194 9 1681 9 0 0 0 0 2194 9 0 0 0 0 +93 wlan0 0x1000310200000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +94 wlan0 0x1000360000000000 10020 0 5606 20 2831 20 5606 20 0 0 0 0 2831 20 0 0 0 0 +95 wlan0 0x1000360000000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +96 wlan0 0x11065fff00000000 10020 0 18363 91 83367 104 18363 91 0 0 0 0 83367 104 0 0 0 0 +97 wlan0 0x11065fff00000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +98 wlan0 0x3000009600000000 10020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +99 wlan0 0x3000009600000000 10020 1 6163 18 2424 18 6163 18 0 0 0 0 2424 18 0 0 0 0 +100 wlan0 0x3000009800000000 10020 0 23337 46 8723 39 23337 46 0 0 0 0 8723 39 0 0 0 0 +101 wlan0 0x3000009800000000 10020 1 33744 93 72437 89 33744 93 0 0 0 0 72437 89 0 0 0 0 +102 wlan0 0x3000020000000000 10020 0 4124 11 8969 19 4124 11 0 0 0 0 8969 19 0 0 0 0 +103 wlan0 0x3000020000000000 10020 1 5993 11 3815 14 5993 11 0 0 0 0 3815 14 0 0 0 0 +104 wlan0 0x3000040100000000 10020 0 113809 342 135666 308 113809 342 0 0 0 0 135666 308 0 0 0 0 +105 wlan0 0x3000040100000000 10020 1 142508 642 500579 637 142508 642 0 0 0 0 500579 637 0 0 0 0 +106 wlan0 0x3000040700000000 10020 0 365815 5119 213340 2733 365815 5119 0 0 0 0 213340 2733 0 0 0 0 +107 wlan0 0x3000040700000000 10020 1 30747 130 18408 100 30747 130 0 0 0 0 18408 100 0 0 0 0 +108 wlan0 0x3000040800000000 10020 0 34672 112 68623 92 34672 112 0 0 0 0 68623 92 0 0 0 0 +109 wlan0 0x3000040800000000 10020 1 78443 199 140944 192 78443 199 0 0 0 0 140944 192 0 0 0 0 +110 wlan0 0x3000040b00000000 10020 0 14949 33 4017 26 14949 33 0 0 0 0 4017 26 0 0 0 0 +111 wlan0 0x3000040b00000000 10020 1 996 15 576 8 996 15 0 0 0 0 576 8 0 0 0 0 +112 wlan0 0x3000090000000000 10020 0 11826 67 7309 52 11826 67 0 0 0 0 7309 52 0 0 0 0 +113 wlan0 0x3000090000000000 10020 1 24805 41 4785 41 24805 41 0 0 0 0 4785 41 0 0 0 0 +114 wlan0 0x3000100300000000 10020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +115 wlan0 0x3000100300000000 10020 1 3112 10 1628 10 3112 10 0 0 0 0 1628 10 0 0 0 0 +116 wlan0 0x3000120300000000 10020 0 38249 107 20374 85 38249 107 0 0 0 0 20374 85 0 0 0 0 +117 wlan0 0x3000120300000000 10020 1 122581 174 36792 143 122581 174 0 0 0 0 36792 143 0 0 0 0 +118 wlan0 0x3000130100000000 10020 0 2700 41 1524 21 2700 41 0 0 0 0 1524 21 0 0 0 0 +119 wlan0 0x3000130100000000 10020 1 22515 59 8366 52 22515 59 0 0 0 0 8366 52 0 0 0 0 +120 wlan0 0x3000180200000000 10020 0 6411 18 14511 20 6411 18 0 0 0 0 14511 20 0 0 0 0 +121 wlan0 0x3000180200000000 10020 1 336 5 319 4 336 5 0 0 0 0 319 4 0 0 0 0 +122 wlan0 0x3000180300000000 10020 0 129301 136 17622 97 129301 136 0 0 0 0 17622 97 0 0 0 0 +123 wlan0 0x3000180300000000 10020 1 464787 429 41703 336 464787 429 0 0 0 0 41703 336 0 0 0 0 +124 wlan0 0x3000180400000000 10020 0 11014 39 2787 25 11014 39 0 0 0 0 2787 25 0 0 0 0 +125 wlan0 0x3000180400000000 10020 1 144040 139 7540 80 144040 139 0 0 0 0 7540 80 0 0 0 0 +126 wlan0 0x3000210100000000 10020 0 10278 44 4579 33 10278 44 0 0 0 0 4579 33 0 0 0 0 +127 wlan0 0x3000210100000000 10020 1 31151 73 14159 47 31151 73 0 0 0 0 14159 47 0 0 0 0 +128 wlan0 0x3000250000000000 10020 0 132 2 72 1 132 2 0 0 0 0 72 1 0 0 0 0 +129 wlan0 0x3000250000000000 10020 1 76614 143 17711 130 76080 137 534 6 0 0 17177 124 534 6 0 0 +130 wlan0 0x3000260100000000 10020 0 9426 26 3535 20 9426 26 0 0 0 0 3535 20 0 0 0 0 +131 wlan0 0x3000260100000000 10020 1 468 7 288 4 468 7 0 0 0 0 288 4 0 0 0 0 +132 wlan0 0x3000300000000000 10020 0 7241 29 12055 26 7241 29 0 0 0 0 12055 26 0 0 0 0 +133 wlan0 0x3000300000000000 10020 1 3273 23 11232 21 3273 23 0 0 0 0 11232 21 0 0 0 0 +134 wlan0 0x3000310000000000 10020 0 132 2 72 1 132 2 0 0 0 0 72 1 0 0 0 0 +135 wlan0 0x3000310000000000 10020 1 53425 64 8721 62 53425 64 0 0 0 0 8721 62 0 0 0 0 +136 wlan0 0x3000310500000000 10020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +137 wlan0 0x3000310500000000 10020 1 9929 16 3879 18 9929 16 0 0 0 0 3879 18 0 0 0 0 +138 wlan0 0x3000320100000000 10020 0 6844 14 3745 13 6844 14 0 0 0 0 3745 13 0 0 0 0 +139 wlan0 0x3000320100000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +140 wlan0 0x3000360000000000 10020 0 8855 43 4749 31 8855 43 0 0 0 0 4749 31 0 0 0 0 +141 wlan0 0x3000360000000000 10020 1 5597 19 2456 19 5597 19 0 0 0 0 2456 19 0 0 0 0 +142 wlan0 0x3010000000000000 10090 0 605140 527 38435 429 605140 527 0 0 0 0 38435 429 0 0 0 0 +143 wlan0 0x3010000000000000 10090 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +144 wlan0 0x31065fff00000000 10020 0 22011 67 29665 64 22011 67 0 0 0 0 29665 64 0 0 0 0 +145 wlan0 0x31065fff00000000 10020 1 10695 34 18347 35 10695 34 0 0 0 0 18347 35 0 0 0 0 +146 wlan0 0x32e544f900000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +147 wlan0 0x32e544f900000000 10034 1 40143 54 7299 61 40143 54 0 0 0 0 7299 61 0 0 0 0 +148 wlan0 0x58872a4400000000 10018 0 4928 11 1669 13 4928 11 0 0 0 0 1669 13 0 0 0 0 +149 wlan0 0x58872a4400000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +150 wlan0 0x5caeaa7b00000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +151 wlan0 0x5caeaa7b00000000 10034 1 74971 73 7103 75 74971 73 0 0 0 0 7103 75 0 0 0 0 +152 wlan0 0x9e00923800000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +153 wlan0 0x9e00923800000000 10034 1 72385 98 13072 110 72385 98 0 0 0 0 13072 110 0 0 0 0 +154 wlan0 0xb972bdd400000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +155 wlan0 0xb972bdd400000000 10034 1 15282 24 3034 27 15282 24 0 0 0 0 3034 27 0 0 0 0 +156 wlan0 0xc7c9f7ba00000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +157 wlan0 0xc7c9f7ba00000000 10034 1 194915 185 13316 138 194915 185 0 0 0 0 13316 138 0 0 0 0 +158 wlan0 0xc9395b2600000000 10034 0 6991 13 6215 14 6991 13 0 0 0 0 6215 14 0 0 0 0 +159 wlan0 0xc9395b2600000000 10034 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +160 wlan0 0xdaddf21100000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +161 wlan0 0xdaddf21100000000 10034 1 928676 849 81570 799 928676 849 0 0 0 0 81570 799 0 0 0 0 +162 wlan0 0xe8d195d100000000 10020 0 516 8 288 4 516 8 0 0 0 0 288 4 0 0 0 0 +163 wlan0 0xe8d195d100000000 10020 1 5905 15 2622 15 5905 15 0 0 0 0 2622 15 0 0 0 0 +164 wlan0 0xe8d195d100000000 10034 0 236640 524 312523 555 236640 524 0 0 0 0 312523 555 0 0 0 0 +165 wlan0 0xe8d195d100000000 10034 1 319028 539 188776 553 319028 539 0 0 0 0 188776 553 0 0 0 0 +166 wlan0 0xffffff0100000000 10006 0 80755 92 9122 99 80755 92 0 0 0 0 9122 99 0 0 0 0 +167 wlan0 0xffffff0100000000 10006 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +168 wlan0 0xffffff0100000000 10020 0 17874405 14068 223987 3065 17874405 14068 0 0 0 0 223987 3065 0 0 0 0 +169 wlan0 0xffffff0100000000 10020 1 11011258 8672 177693 2407 11011258 8672 0 0 0 0 177693 2407 0 0 0 0 +170 wlan0 0xffffff0100000000 10034 0 436062595 341880 5843990 79630 436062595 341880 0 0 0 0 5843990 79630 0 0 0 0 +171 wlan0 0xffffff0100000000 10034 1 63201220 49447 1005882 13713 63201220 49447 0 0 0 0 1005882 13713 0 0 0 0 +172 wlan0 0xffffff0100000000 10044 0 17159287 13702 356212 4778 17159287 13702 0 0 0 0 356212 4778 0 0 0 0 +173 wlan0 0xffffff0100000000 10044 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +174 wlan0 0xffffff0100000000 10078 0 10439 17 1665 15 10439 17 0 0 0 0 1665 15 0 0 0 0 +175 wlan0 0xffffff0100000000 10078 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +176 wlan0 0xffffff0100000000 10090 0 23722655 19697 881995 14231 23722655 19697 0 0 0 0 881995 14231 0 0 0 0 +177 wlan0 0xffffff0100000000 10090 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +178 wlan0 0xffffff0500000000 1000 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +179 wlan0 0xffffff0500000000 1000 1 1592 5 314 1 0 0 1592 5 0 0 0 0 314 1 0 0 +180 wlan0 0xffffff0600000000 1000 0 0 0 36960 385 0 0 0 0 0 0 0 0 36960 385 0 0 +181 wlan0 0xffffff0600000000 1000 1 96 1 480 5 0 0 96 1 0 0 0 0 480 5 0 0 +182 wlan0 0xffffff0700000000 1000 0 38732 229 16567 163 38732 229 0 0 0 0 16567 163 0 0 0 0 +183 wlan0 0xffffff0700000000 1000 1 18539 74 7562 66 18539 74 0 0 0 0 7562 66 0 0 0 0 +184 wlan0 0xffffff0900000000 1000 0 38381 43 2624 27 38381 43 0 0 0 0 2624 27 0 0 0 0 +185 wlan0 0xffffff0900000000 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +186 dummy0 0x0 0 0 0 0 168 3 0 0 0 0 0 0 0 0 0 0 168 3 +187 dummy0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +188 wlan0 0x0 1029 0 0 0 8524052 130894 0 0 0 0 0 0 7871216 121284 108568 1325 544268 8285 +189 wlan0 0x0 1029 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/tests/unit/res/raw/xt_qtaguid_with_clat_100mb_download_before b/tests/unit/res/raw/xt_qtaguid_with_clat_100mb_download_before new file mode 100644 index 0000000000..ce4bcc3a3b --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_with_clat_100mb_download_before @@ -0,0 +1,187 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 r_rmnet_data0 0x0 0 0 0 0 392 6 0 0 0 0 0 0 0 0 0 0 392 6 +3 r_rmnet_data0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +4 v4-wlan0 0x0 0 0 58848 2070 2836 64 160 4 0 0 58688 2066 80 2 0 0 2756 62 +5 v4-wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +6 v4-wlan0 0x0 10034 0 6192 11 1445 11 6192 11 0 0 0 0 1445 11 0 0 0 0 +7 v4-wlan0 0x0 10034 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +8 v4-wlan0 0x0 10057 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +9 v4-wlan0 0x0 10057 1 728 7 392 7 0 0 728 7 0 0 0 0 392 7 0 0 +10 v4-wlan0 0x0 10106 0 1488 12 1488 12 0 0 1488 12 0 0 0 0 1488 12 0 0 +11 v4-wlan0 0x0 10106 1 323981189 235142 3509032 84542 323979453 235128 1736 14 0 0 3502676 84363 6356 179 0 0 +12 wlan0 0x0 0 0 330187296 250652 0 0 329106990 236273 226202 1255 854104 13124 0 0 0 0 0 0 +13 wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +14 wlan0 0x0 1000 0 77113 272 56151 575 77113 272 0 0 0 0 19191 190 36960 385 0 0 +15 wlan0 0x0 1000 1 20227 80 8356 72 18539 74 1688 6 0 0 7562 66 794 6 0 0 +16 wlan0 0x0 10006 0 80755 92 9122 99 80755 92 0 0 0 0 9122 99 0 0 0 0 +17 wlan0 0x0 10006 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +18 wlan0 0x0 10015 0 4390 7 14824 252 4390 7 0 0 0 0 14824 252 0 0 0 0 +19 wlan0 0x0 10015 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +20 wlan0 0x0 10018 0 4928 11 1741 14 4928 11 0 0 0 0 1741 14 0 0 0 0 +21 wlan0 0x0 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +22 wlan0 0x0 10020 0 21141412 34316 2329881 15262 21140807 34311 605 5 0 0 2329276 15257 605 5 0 0 +23 wlan0 0x0 10020 1 13835740 12938 1548555 6362 13833754 12920 1986 18 0 0 1546569 6344 1986 18 0 0 +24 wlan0 0x0 10023 0 13405 40 5042 44 13405 40 0 0 0 0 5042 44 0 0 0 0 +25 wlan0 0x0 10023 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +26 wlan0 0x0 10034 0 436394741 342648 6237981 80442 436394741 342648 0 0 0 0 6237981 80442 0 0 0 0 +27 wlan0 0x0 10034 1 64860872 51297 1335539 15546 64860872 51297 0 0 0 0 1335539 15546 0 0 0 0 +28 wlan0 0x0 10044 0 17614444 14774 521004 5694 17329882 14432 284562 342 0 0 419974 5408 101030 286 0 0 +29 wlan0 0x0 10044 1 17701 33 3100 28 17701 33 0 0 0 0 3100 28 0 0 0 0 +30 wlan0 0x0 10057 0 12311735 9335 435954 5448 12247721 9259 64014 76 0 0 414080 5386 21874 62 0 0 +31 wlan0 0x0 10057 1 1332953195 954797 31849632 457698 1331933207 953569 1019988 1228 0 0 31702284 456899 147348 799 0 0 +32 wlan0 0x0 10060 0 32972 200 433705 380 32972 200 0 0 0 0 433705 380 0 0 0 0 +33 wlan0 0x0 10060 1 32106 66 37789 87 32106 66 0 0 0 0 37789 87 0 0 0 0 +34 wlan0 0x0 10061 0 7675 23 2509 22 7675 23 0 0 0 0 2509 22 0 0 0 0 +35 wlan0 0x0 10061 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +36 wlan0 0x0 10074 0 38355 82 10447 97 38355 82 0 0 0 0 10447 97 0 0 0 0 +37 wlan0 0x0 10074 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +38 wlan0 0x0 10078 0 49013 79 7167 69 49013 79 0 0 0 0 7167 69 0 0 0 0 +39 wlan0 0x0 10078 1 5872 8 1236 10 5872 8 0 0 0 0 1236 10 0 0 0 0 +40 wlan0 0x0 10082 0 8301 13 1981 15 8301 13 0 0 0 0 1981 15 0 0 0 0 +41 wlan0 0x0 10082 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +42 wlan0 0x0 10086 0 7001 14 1579 15 7001 14 0 0 0 0 1579 15 0 0 0 0 +43 wlan0 0x0 10086 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +44 wlan0 0x0 10090 0 24327795 20224 920502 14661 24327795 20224 0 0 0 0 920502 14661 0 0 0 0 +45 wlan0 0x0 10090 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +46 wlan0 0x0 10092 0 36849 78 12449 81 36849 78 0 0 0 0 12449 81 0 0 0 0 +47 wlan0 0x0 10092 1 60 1 103 1 60 1 0 0 0 0 103 1 0 0 0 0 +48 wlan0 0x0 10095 0 131962 223 37069 241 131962 223 0 0 0 0 37069 241 0 0 0 0 +49 wlan0 0x0 10095 1 12949 21 3930 21 12949 21 0 0 0 0 3930 21 0 0 0 0 +50 wlan0 0x0 10106 0 30899554 22679 632476 12296 30895334 22645 4220 34 0 0 628256 12262 4220 34 0 0 +51 wlan0 0x0 10106 1 88922349 64952 1605126 35599 88916075 64875 3586 29 2688 48 1600196 35522 4930 77 0 0 +52 wlan0 0x40700000000 10020 0 705732 10589 404428 5504 705732 10589 0 0 0 0 404428 5504 0 0 0 0 +53 wlan0 0x40700000000 10020 1 2376 36 1296 18 2376 36 0 0 0 0 1296 18 0 0 0 0 +54 wlan0 0x40800000000 10020 0 34624 146 122525 160 34624 146 0 0 0 0 122525 160 0 0 0 0 +55 wlan0 0x40800000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +56 wlan0 0x40b00000000 10020 0 22411 85 7364 57 22411 85 0 0 0 0 7364 57 0 0 0 0 +57 wlan0 0x40b00000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +58 wlan0 0x120300000000 10020 0 76641 241 32783 169 76641 241 0 0 0 0 32783 169 0 0 0 0 +59 wlan0 0x120300000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +60 wlan0 0x130100000000 10020 0 73101 287 23236 203 73101 287 0 0 0 0 23236 203 0 0 0 0 +61 wlan0 0x130100000000 10020 1 264 4 144 2 264 4 0 0 0 0 144 2 0 0 0 0 +62 wlan0 0x180300000000 10020 0 330648 399 24736 232 330648 399 0 0 0 0 24736 232 0 0 0 0 +63 wlan0 0x180300000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +64 wlan0 0x180400000000 10020 0 21865 59 5022 42 21865 59 0 0 0 0 5022 42 0 0 0 0 +65 wlan0 0x180400000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +66 wlan0 0x300000000000 10020 0 15984 65 26927 57 15984 65 0 0 0 0 26927 57 0 0 0 0 +67 wlan0 0x300000000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +68 wlan0 0x1065fff00000000 10020 0 131871 599 93783 445 131871 599 0 0 0 0 93783 445 0 0 0 0 +69 wlan0 0x1065fff00000000 10020 1 264 4 144 2 264 4 0 0 0 0 144 2 0 0 0 0 +70 wlan0 0x1b24f4600000000 10034 0 15445 42 23329 45 15445 42 0 0 0 0 23329 45 0 0 0 0 +71 wlan0 0x1b24f4600000000 10034 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +72 wlan0 0x1000010000000000 10020 0 5542 9 1364 10 5542 9 0 0 0 0 1364 10 0 0 0 0 +73 wlan0 0x1000010000000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +74 wlan0 0x1000040100000000 10020 0 47196 184 213319 257 47196 184 0 0 0 0 213319 257 0 0 0 0 +75 wlan0 0x1000040100000000 10020 1 60 1 103 1 60 1 0 0 0 0 103 1 0 0 0 0 +76 wlan0 0x1000040700000000 10020 0 11599 50 10786 47 11599 50 0 0 0 0 10786 47 0 0 0 0 +77 wlan0 0x1000040700000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +78 wlan0 0x1000040800000000 10020 0 21902 145 174139 166 21902 145 0 0 0 0 174139 166 0 0 0 0 +79 wlan0 0x1000040800000000 10020 1 8568 88 105743 90 8568 88 0 0 0 0 105743 90 0 0 0 0 +80 wlan0 0x1000100300000000 10020 0 55213 118 194551 199 55213 118 0 0 0 0 194551 199 0 0 0 0 +81 wlan0 0x1000100300000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +82 wlan0 0x1000120300000000 10020 0 50826 74 21153 70 50826 74 0 0 0 0 21153 70 0 0 0 0 +83 wlan0 0x1000120300000000 10020 1 72 1 175 2 72 1 0 0 0 0 175 2 0 0 0 0 +84 wlan0 0x1000180300000000 10020 0 744198 657 65437 592 744198 657 0 0 0 0 65437 592 0 0 0 0 +85 wlan0 0x1000180300000000 10020 1 144719 132 10989 108 144719 132 0 0 0 0 10989 108 0 0 0 0 +86 wlan0 0x1000180600000000 10020 0 4599 8 1928 10 4599 8 0 0 0 0 1928 10 0 0 0 0 +87 wlan0 0x1000180600000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +88 wlan0 0x1000250000000000 10020 0 57740 98 13076 88 57740 98 0 0 0 0 13076 88 0 0 0 0 +89 wlan0 0x1000250000000000 10020 1 328 3 414 4 207 2 121 1 0 0 293 3 121 1 0 0 +90 wlan0 0x1000300000000000 10020 0 7675 30 31331 32 7675 30 0 0 0 0 31331 32 0 0 0 0 +91 wlan0 0x1000300000000000 10020 1 30173 97 101335 100 30173 97 0 0 0 0 101335 100 0 0 0 0 +92 wlan0 0x1000310200000000 10020 0 1681 9 2194 9 1681 9 0 0 0 0 2194 9 0 0 0 0 +93 wlan0 0x1000310200000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +94 wlan0 0x1000360000000000 10020 0 5606 20 2831 20 5606 20 0 0 0 0 2831 20 0 0 0 0 +95 wlan0 0x1000360000000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +96 wlan0 0x11065fff00000000 10020 0 18363 91 83367 104 18363 91 0 0 0 0 83367 104 0 0 0 0 +97 wlan0 0x11065fff00000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +98 wlan0 0x3000009600000000 10020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +99 wlan0 0x3000009600000000 10020 1 6163 18 2424 18 6163 18 0 0 0 0 2424 18 0 0 0 0 +100 wlan0 0x3000009800000000 10020 0 23337 46 8723 39 23337 46 0 0 0 0 8723 39 0 0 0 0 +101 wlan0 0x3000009800000000 10020 1 33744 93 72437 89 33744 93 0 0 0 0 72437 89 0 0 0 0 +102 wlan0 0x3000020000000000 10020 0 4124 11 8969 19 4124 11 0 0 0 0 8969 19 0 0 0 0 +103 wlan0 0x3000020000000000 10020 1 5993 11 3815 14 5993 11 0 0 0 0 3815 14 0 0 0 0 +104 wlan0 0x3000040100000000 10020 0 106718 322 121557 287 106718 322 0 0 0 0 121557 287 0 0 0 0 +105 wlan0 0x3000040100000000 10020 1 142508 642 500579 637 142508 642 0 0 0 0 500579 637 0 0 0 0 +106 wlan0 0x3000040700000000 10020 0 365419 5113 213124 2730 365419 5113 0 0 0 0 213124 2730 0 0 0 0 +107 wlan0 0x3000040700000000 10020 1 30747 130 18408 100 30747 130 0 0 0 0 18408 100 0 0 0 0 +108 wlan0 0x3000040800000000 10020 0 34672 112 68623 92 34672 112 0 0 0 0 68623 92 0 0 0 0 +109 wlan0 0x3000040800000000 10020 1 78443 199 140944 192 78443 199 0 0 0 0 140944 192 0 0 0 0 +110 wlan0 0x3000040b00000000 10020 0 14949 33 4017 26 14949 33 0 0 0 0 4017 26 0 0 0 0 +111 wlan0 0x3000040b00000000 10020 1 996 15 576 8 996 15 0 0 0 0 576 8 0 0 0 0 +112 wlan0 0x3000090000000000 10020 0 4017 28 3610 25 4017 28 0 0 0 0 3610 25 0 0 0 0 +113 wlan0 0x3000090000000000 10020 1 24805 41 4545 38 24805 41 0 0 0 0 4545 38 0 0 0 0 +114 wlan0 0x3000100300000000 10020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +115 wlan0 0x3000100300000000 10020 1 3112 10 1628 10 3112 10 0 0 0 0 1628 10 0 0 0 0 +116 wlan0 0x3000120300000000 10020 0 38249 107 20374 85 38249 107 0 0 0 0 20374 85 0 0 0 0 +117 wlan0 0x3000120300000000 10020 1 122581 174 36792 143 122581 174 0 0 0 0 36792 143 0 0 0 0 +118 wlan0 0x3000130100000000 10020 0 2700 41 1524 21 2700 41 0 0 0 0 1524 21 0 0 0 0 +119 wlan0 0x3000130100000000 10020 1 22515 59 8366 52 22515 59 0 0 0 0 8366 52 0 0 0 0 +120 wlan0 0x3000180200000000 10020 0 6411 18 14511 20 6411 18 0 0 0 0 14511 20 0 0 0 0 +121 wlan0 0x3000180200000000 10020 1 336 5 319 4 336 5 0 0 0 0 319 4 0 0 0 0 +122 wlan0 0x3000180300000000 10020 0 129301 136 17622 97 129301 136 0 0 0 0 17622 97 0 0 0 0 +123 wlan0 0x3000180300000000 10020 1 464787 429 41703 336 464787 429 0 0 0 0 41703 336 0 0 0 0 +124 wlan0 0x3000180400000000 10020 0 11014 39 2787 25 11014 39 0 0 0 0 2787 25 0 0 0 0 +125 wlan0 0x3000180400000000 10020 1 144040 139 7540 80 144040 139 0 0 0 0 7540 80 0 0 0 0 +126 wlan0 0x3000210100000000 10020 0 10278 44 4579 33 10278 44 0 0 0 0 4579 33 0 0 0 0 +127 wlan0 0x3000210100000000 10020 1 31151 73 14159 47 31151 73 0 0 0 0 14159 47 0 0 0 0 +128 wlan0 0x3000250000000000 10020 0 132 2 72 1 132 2 0 0 0 0 72 1 0 0 0 0 +129 wlan0 0x3000250000000000 10020 1 76614 143 17711 130 76080 137 534 6 0 0 17177 124 534 6 0 0 +130 wlan0 0x3000260100000000 10020 0 9426 26 3535 20 9426 26 0 0 0 0 3535 20 0 0 0 0 +131 wlan0 0x3000260100000000 10020 1 468 7 288 4 468 7 0 0 0 0 288 4 0 0 0 0 +132 wlan0 0x3000300000000000 10020 0 7241 29 12055 26 7241 29 0 0 0 0 12055 26 0 0 0 0 +133 wlan0 0x3000300000000000 10020 1 3273 23 11232 21 3273 23 0 0 0 0 11232 21 0 0 0 0 +134 wlan0 0x3000310000000000 10020 0 132 2 72 1 132 2 0 0 0 0 72 1 0 0 0 0 +135 wlan0 0x3000310000000000 10020 1 53425 64 8721 62 53425 64 0 0 0 0 8721 62 0 0 0 0 +136 wlan0 0x3000310500000000 10020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +137 wlan0 0x3000310500000000 10020 1 9929 16 3879 18 9929 16 0 0 0 0 3879 18 0 0 0 0 +138 wlan0 0x3000360000000000 10020 0 8855 43 4749 31 8855 43 0 0 0 0 4749 31 0 0 0 0 +139 wlan0 0x3000360000000000 10020 1 5597 19 2456 19 5597 19 0 0 0 0 2456 19 0 0 0 0 +140 wlan0 0x3010000000000000 10090 0 605140 527 38435 429 605140 527 0 0 0 0 38435 429 0 0 0 0 +141 wlan0 0x3010000000000000 10090 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +142 wlan0 0x31065fff00000000 10020 0 22011 67 29665 64 22011 67 0 0 0 0 29665 64 0 0 0 0 +143 wlan0 0x31065fff00000000 10020 1 10695 34 18347 35 10695 34 0 0 0 0 18347 35 0 0 0 0 +144 wlan0 0x32e544f900000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +145 wlan0 0x32e544f900000000 10034 1 40143 54 7299 61 40143 54 0 0 0 0 7299 61 0 0 0 0 +146 wlan0 0x58872a4400000000 10018 0 4928 11 1669 13 4928 11 0 0 0 0 1669 13 0 0 0 0 +147 wlan0 0x58872a4400000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +148 wlan0 0x5caeaa7b00000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +149 wlan0 0x5caeaa7b00000000 10034 1 74971 73 7103 75 74971 73 0 0 0 0 7103 75 0 0 0 0 +150 wlan0 0x9e00923800000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +151 wlan0 0x9e00923800000000 10034 1 72385 98 13072 110 72385 98 0 0 0 0 13072 110 0 0 0 0 +152 wlan0 0xb972bdd400000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +153 wlan0 0xb972bdd400000000 10034 1 15282 24 3034 27 15282 24 0 0 0 0 3034 27 0 0 0 0 +154 wlan0 0xc7c9f7ba00000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +155 wlan0 0xc7c9f7ba00000000 10034 1 194915 185 13316 138 194915 185 0 0 0 0 13316 138 0 0 0 0 +156 wlan0 0xc9395b2600000000 10034 0 6991 13 6215 14 6991 13 0 0 0 0 6215 14 0 0 0 0 +157 wlan0 0xc9395b2600000000 10034 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +158 wlan0 0xdaddf21100000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +159 wlan0 0xdaddf21100000000 10034 1 928676 849 81570 799 928676 849 0 0 0 0 81570 799 0 0 0 0 +160 wlan0 0xe8d195d100000000 10020 0 516 8 288 4 516 8 0 0 0 0 288 4 0 0 0 0 +161 wlan0 0xe8d195d100000000 10020 1 5905 15 2622 15 5905 15 0 0 0 0 2622 15 0 0 0 0 +162 wlan0 0xe8d195d100000000 10034 0 236640 524 312523 555 236640 524 0 0 0 0 312523 555 0 0 0 0 +163 wlan0 0xe8d195d100000000 10034 1 319028 539 188776 553 319028 539 0 0 0 0 188776 553 0 0 0 0 +164 wlan0 0xffffff0100000000 10006 0 80755 92 9122 99 80755 92 0 0 0 0 9122 99 0 0 0 0 +165 wlan0 0xffffff0100000000 10006 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +166 wlan0 0xffffff0100000000 10020 0 17874405 14068 223987 3065 17874405 14068 0 0 0 0 223987 3065 0 0 0 0 +167 wlan0 0xffffff0100000000 10020 1 11011258 8672 177693 2407 11011258 8672 0 0 0 0 177693 2407 0 0 0 0 +168 wlan0 0xffffff0100000000 10034 0 436062595 341880 5843990 79630 436062595 341880 0 0 0 0 5843990 79630 0 0 0 0 +169 wlan0 0xffffff0100000000 10034 1 63201220 49447 1005882 13713 63201220 49447 0 0 0 0 1005882 13713 0 0 0 0 +170 wlan0 0xffffff0100000000 10044 0 17159287 13702 356212 4778 17159287 13702 0 0 0 0 356212 4778 0 0 0 0 +171 wlan0 0xffffff0100000000 10044 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +172 wlan0 0xffffff0100000000 10078 0 10439 17 1665 15 10439 17 0 0 0 0 1665 15 0 0 0 0 +173 wlan0 0xffffff0100000000 10078 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +174 wlan0 0xffffff0100000000 10090 0 23722655 19697 881995 14231 23722655 19697 0 0 0 0 881995 14231 0 0 0 0 +175 wlan0 0xffffff0100000000 10090 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +176 wlan0 0xffffff0500000000 1000 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +177 wlan0 0xffffff0500000000 1000 1 1592 5 314 1 0 0 1592 5 0 0 0 0 314 1 0 0 +178 wlan0 0xffffff0600000000 1000 0 0 0 36960 385 0 0 0 0 0 0 0 0 36960 385 0 0 +179 wlan0 0xffffff0600000000 1000 1 96 1 480 5 0 0 96 1 0 0 0 0 480 5 0 0 +180 wlan0 0xffffff0700000000 1000 0 38732 229 16567 163 38732 229 0 0 0 0 16567 163 0 0 0 0 +181 wlan0 0xffffff0700000000 1000 1 18539 74 7562 66 18539 74 0 0 0 0 7562 66 0 0 0 0 +182 wlan0 0xffffff0900000000 1000 0 38381 43 2624 27 38381 43 0 0 0 0 2624 27 0 0 0 0 +183 wlan0 0xffffff0900000000 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +184 dummy0 0x0 0 0 0 0 168 3 0 0 0 0 0 0 0 0 0 0 168 3 +185 dummy0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +186 wlan0 0x0 1029 0 0 0 5855801 94173 0 0 0 0 0 0 5208040 84634 103637 1256 544124 8283 +187 wlan0 0x0 1029 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/tests/unit/res/raw/xt_qtaguid_with_clat_simple b/tests/unit/res/raw/xt_qtaguid_with_clat_simple new file mode 100644 index 0000000000..a1d6d411ba --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_with_clat_simple @@ -0,0 +1,4 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 v4-wlan0 0x0 10060 0 42600 213 4100 41 42600 213 0 0 0 0 4100 41 0 0 0 0 +3 v4-wlan0 0x0 10060 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +4 wlan0 0x0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0