diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp new file mode 100644 index 0000000000..47b114b64d --- /dev/null +++ b/tests/cts/hostside/Android.bp @@ -0,0 +1,29 @@ +// 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. + +java_test_host { + name: "CtsHostsideNetworkTests", + defaults: ["cts_defaults"], + // Only compile source java files in this apk. + srcs: ["src/**/*.java"], + libs: [ + "cts-tradefed", + "tradefed", + ], + // Tag this module as a cts test artifact + test_suites: [ + "cts", + "general-tests", + ], +} diff --git a/tests/cts/hostside/AndroidTest.xml b/tests/cts/hostside/AndroidTest.xml new file mode 100644 index 0000000000..b7fefaf3b5 --- /dev/null +++ b/tests/cts/hostside/AndroidTest.xml @@ -0,0 +1,41 @@ + + + + diff --git a/tests/cts/hostside/OWNERS b/tests/cts/hostside/OWNERS new file mode 100644 index 0000000000..52c8053323 --- /dev/null +++ b/tests/cts/hostside/OWNERS @@ -0,0 +1,4 @@ +# Bug component: 61373 +sudheersai@google.com +lorenzo@google.com +jchalard@google.com diff --git a/tests/cts/hostside/aidl/Android.bp b/tests/cts/hostside/aidl/Android.bp new file mode 100644 index 0000000000..320a1fa443 --- /dev/null +++ b/tests/cts/hostside/aidl/Android.bp @@ -0,0 +1,24 @@ +// 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. + +java_test_helper_library { + name: "CtsHostsideNetworkTestsAidl", + sdk_version: "current", + srcs: [ + "com/android/cts/net/hostside/IMyService.aidl", + "com/android/cts/net/hostside/INetworkCallback.aidl", + "com/android/cts/net/hostside/INetworkStateObserver.aidl", + "com/android/cts/net/hostside/IRemoteSocketFactory.aidl", + ], +} diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl new file mode 100644 index 0000000000..5aafdf06cb --- /dev/null +++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl @@ -0,0 +1,29 @@ +/* + * 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.cts.net.hostside; + +import com.android.cts.net.hostside.INetworkCallback; + +interface IMyService { + void registerBroadcastReceiver(); + int getCounters(String receiverName, String action); + String checkNetworkStatus(); + String getRestrictBackgroundStatus(); + void sendNotification(int notificationId, String notificationType); + void registerNetworkCallback(in INetworkCallback cb); + void unregisterNetworkCallback(); +} diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkCallback.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkCallback.aidl new file mode 100644 index 0000000000..2048bab498 --- /dev/null +++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkCallback.aidl @@ -0,0 +1,27 @@ +/* + * 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.cts.net.hostside; + +import android.net.Network; +import android.net.NetworkCapabilities; + +interface INetworkCallback { + void onBlockedStatusChanged(in Network network, boolean blocked); + void onAvailable(in Network network); + void onLost(in Network network); + void onCapabilitiesChanged(in Network network, in NetworkCapabilities cap); +} diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl new file mode 100644 index 0000000000..165f5306c3 --- /dev/null +++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl @@ -0,0 +1,22 @@ +/* + * 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.cts.net.hostside; + +interface INetworkStateObserver { + boolean isForeground(); + void onNetworkStateChecked(String resultData); +} \ No newline at end of file diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IRemoteSocketFactory.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IRemoteSocketFactory.aidl new file mode 100644 index 0000000000..68176ad80d --- /dev/null +++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IRemoteSocketFactory.aidl @@ -0,0 +1,25 @@ +/* + * 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.cts.net.hostside; + +import android.os.ParcelFileDescriptor; + +interface IRemoteSocketFactory { + ParcelFileDescriptor openSocketFd(String host, int port, int timeoutMs); + String getPackageName(); + int getUid(); +} diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp new file mode 100644 index 0000000000..99037567b5 --- /dev/null +++ b/tests/cts/hostside/app/Android.bp @@ -0,0 +1,40 @@ +// +// 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. +// + +android_test_helper_app { + name: "CtsHostsideNetworkTestsApp", + defaults: ["cts_support_defaults"], + //sdk_version: "current", + platform_apis: true, + static_libs: [ + "androidx.test.rules", + "androidx.test.ext.junit", + "compatibility-device-util-axt", + "ctstestrunner-axt", + "ub-uiautomator", + "CtsHostsideNetworkTestsAidl", + ], + libs: [ + "android.test.runner", + "android.test.base", + ], + srcs: ["src/**/*.java"], + // Tag this module as a cts test artifact + test_suites: [ + "cts", + "general-tests", + ], +} diff --git a/tests/cts/hostside/app/AndroidManifest.xml b/tests/cts/hostside/app/AndroidManifest.xml new file mode 100644 index 0000000000..3940de4240 --- /dev/null +++ b/tests/cts/hostside/app/AndroidManifest.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractAppIdleTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractAppIdleTestCase.java new file mode 100644 index 0000000000..f9e30b6b20 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractAppIdleTestCase.java @@ -0,0 +1,201 @@ +/* + * 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.cts.net.hostside; + +import static com.android.cts.net.hostside.Property.APP_STANDBY_MODE; +import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE; + +import static org.junit.Assert.assertEquals; + +import android.os.SystemClock; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Base class for metered and non-metered tests on idle apps. + */ +@RequiredProperties({APP_STANDBY_MODE}) +abstract class AbstractAppIdleTestCase extends AbstractRestrictBackgroundNetworkTestCase { + + @Before + public final void setUp() throws Exception { + super.setUp(); + + // Set initial state. + removePowerSaveModeWhitelist(TEST_APP2_PKG); + removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG); + setAppIdle(false); + turnBatteryOn(); + + registerBroadcastReceiver(); + } + + @After + public final void tearDown() throws Exception { + super.tearDown(); + + executeSilentShellCommand("cmd battery reset"); + setAppIdle(false); + } + + @Test + public void testBackgroundNetworkAccess_enabled() throws Exception { + setAppIdle(true); + assertBackgroundNetworkAccess(false); + + assertsForegroundAlwaysHasNetworkAccess(); + setAppIdle(true); + assertBackgroundNetworkAccess(false); + + // Make sure foreground app doesn't lose access upon enabling it. + setAppIdle(true); + launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY); + finishActivity(); + assertAppIdle(false); // verify - not idle anymore, since activity was launched... + assertBackgroundNetworkAccess(true); + setAppIdle(true); + assertBackgroundNetworkAccess(false); + + // Same for foreground service. + setAppIdle(true); + launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE); + stopForegroundService(); + assertAppIdle(true); + assertBackgroundNetworkAccess(false); + + // Set Idle after foreground service start. + launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE); + setAppIdle(true); + addPowerSaveModeWhitelist(TEST_PKG); + removePowerSaveModeWhitelist(TEST_PKG); + assertForegroundServiceNetworkAccess(); + stopForegroundService(); + assertAppIdle(true); + assertBackgroundNetworkAccess(false); + + } + + @Test + public void testBackgroundNetworkAccess_whitelisted() throws Exception { + setAppIdle(true); + assertBackgroundNetworkAccess(false); + + addPowerSaveModeWhitelist(TEST_APP2_PKG); + assertAppIdle(false); // verify - not idle anymore, since whitelisted + assertBackgroundNetworkAccess(true); + + setAppIdleNoAssert(true); + assertAppIdle(false); // app is still whitelisted + removePowerSaveModeWhitelist(TEST_APP2_PKG); + assertAppIdle(true); // verify - idle again, once whitelisted was removed + assertBackgroundNetworkAccess(false); + + setAppIdle(true); + addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG); + assertAppIdle(false); // verify - not idle anymore, since whitelisted + assertBackgroundNetworkAccess(true); + + setAppIdleNoAssert(true); + assertAppIdle(false); // app is still whitelisted + removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG); + assertAppIdle(true); // verify - idle again, once whitelisted was removed + assertBackgroundNetworkAccess(false); + + assertsForegroundAlwaysHasNetworkAccess(); + + // verify - no whitelist, no access! + setAppIdle(true); + assertBackgroundNetworkAccess(false); + } + + @Test + public void testBackgroundNetworkAccess_tempWhitelisted() throws Exception { + setAppIdle(true); + assertBackgroundNetworkAccess(false); + + addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS); + assertBackgroundNetworkAccess(true); + // Wait until the whitelist duration is expired. + SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS); + assertBackgroundNetworkAccess(false); + } + + @Test + public void testBackgroundNetworkAccess_disabled() throws Exception { + assertBackgroundNetworkAccess(true); + + assertsForegroundAlwaysHasNetworkAccess(); + assertBackgroundNetworkAccess(true); + } + + @RequiredProperties({BATTERY_SAVER_MODE}) + @Test + public void testAppIdleNetworkAccess_whenCharging() throws Exception { + // Check that app is paroled when charging + setAppIdle(true); + assertBackgroundNetworkAccess(false); + turnBatteryOff(); + assertBackgroundNetworkAccess(true); + turnBatteryOn(); + assertBackgroundNetworkAccess(false); + + // Check that app is restricted when not idle but power-save is on + setAppIdle(false); + assertBackgroundNetworkAccess(true); + setBatterySaverMode(true); + assertBackgroundNetworkAccess(false); + // Use setBatterySaverMode API to leave power-save mode instead of plugging in charger + setBatterySaverMode(false); + turnBatteryOff(); + assertBackgroundNetworkAccess(true); + + // And when no longer charging, it still has network access, since it's not idle + turnBatteryOn(); + assertBackgroundNetworkAccess(true); + } + + @Test + public void testAppIdleNetworkAccess_idleWhitelisted() throws Exception { + setAppIdle(true); + assertAppIdle(true); + assertBackgroundNetworkAccess(false); + + addAppIdleWhitelist(mUid); + assertBackgroundNetworkAccess(true); + + removeAppIdleWhitelist(mUid); + assertBackgroundNetworkAccess(false); + + // Make sure whitelisting a random app doesn't affect the tested app. + addAppIdleWhitelist(mUid + 1); + assertBackgroundNetworkAccess(false); + removeAppIdleWhitelist(mUid + 1); + } + + @Test + public void testAppIdle_toast() throws Exception { + setAppIdle(true); + assertAppIdle(true); + assertEquals("Shown", showToast()); + assertAppIdle(true); + // Wait for a couple of seconds for the toast to actually be shown + SystemClock.sleep(2000); + assertAppIdle(true); + } +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractBatterySaverModeTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractBatterySaverModeTestCase.java new file mode 100644 index 0000000000..04d054d54a --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractBatterySaverModeTestCase.java @@ -0,0 +1,111 @@ +/* + * 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.cts.net.hostside; + +import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Base class for metered and non-metered Battery Saver Mode tests. + */ +@RequiredProperties({BATTERY_SAVER_MODE}) +abstract class AbstractBatterySaverModeTestCase extends AbstractRestrictBackgroundNetworkTestCase { + + @Before + public final void setUp() throws Exception { + super.setUp(); + + // Set initial state. + removePowerSaveModeWhitelist(TEST_APP2_PKG); + removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG); + setBatterySaverMode(false); + + registerBroadcastReceiver(); + } + + @After + public final void tearDown() throws Exception { + super.tearDown(); + + setBatterySaverMode(false); + } + + @Test + public void testBackgroundNetworkAccess_enabled() throws Exception { + setBatterySaverMode(true); + assertBackgroundNetworkAccess(false); + + assertsForegroundAlwaysHasNetworkAccess(); + assertBackgroundNetworkAccess(false); + + // Make sure foreground app doesn't lose access upon Battery Saver. + setBatterySaverMode(false); + launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY); + setBatterySaverMode(true); + assertForegroundNetworkAccess(); + + // Although it should not have access while the screen is off. + turnScreenOff(); + assertBackgroundNetworkAccess(false); + turnScreenOn(); + assertForegroundNetworkAccess(); + + // Goes back to background state. + finishActivity(); + assertBackgroundNetworkAccess(false); + + // Make sure foreground service doesn't lose access upon enabling Battery Saver. + setBatterySaverMode(false); + launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE); + setBatterySaverMode(true); + assertForegroundNetworkAccess(); + stopForegroundService(); + assertBackgroundNetworkAccess(false); + } + + @Test + public void testBackgroundNetworkAccess_whitelisted() throws Exception { + setBatterySaverMode(true); + assertBackgroundNetworkAccess(false); + + addPowerSaveModeWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(true); + + removePowerSaveModeWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(false); + + addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(true); + + removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(false); + + assertsForegroundAlwaysHasNetworkAccess(); + assertBackgroundNetworkAccess(false); + } + + @Test + public void testBackgroundNetworkAccess_disabled() throws Exception { + assertBackgroundNetworkAccess(true); + + assertsForegroundAlwaysHasNetworkAccess(); + assertBackgroundNetworkAccess(true); + } +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java new file mode 100644 index 0000000000..6f32c563c1 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java @@ -0,0 +1,141 @@ +/* + * 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.cts.net.hostside; + +import static com.android.cts.net.hostside.Property.DOZE_MODE; +import static com.android.cts.net.hostside.Property.NOT_LOW_RAM_DEVICE; + +import android.os.SystemClock; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Base class for metered and non-metered Doze Mode tests. + */ +@RequiredProperties({DOZE_MODE}) +abstract class AbstractDozeModeTestCase extends AbstractRestrictBackgroundNetworkTestCase { + + @Before + public final void setUp() throws Exception { + super.setUp(); + + // Set initial state. + removePowerSaveModeWhitelist(TEST_APP2_PKG); + removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG); + setDozeMode(false); + + registerBroadcastReceiver(); + } + + @After + public final void tearDown() throws Exception { + super.tearDown(); + + setDozeMode(false); + } + + @Test + public void testBackgroundNetworkAccess_enabled() throws Exception { + setDozeMode(true); + assertBackgroundNetworkAccess(false); + + assertsForegroundAlwaysHasNetworkAccess(); + assertBackgroundNetworkAccess(false); + + // Make sure foreground service doesn't lose network access upon enabling doze. + setDozeMode(false); + launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE); + setDozeMode(true); + assertForegroundNetworkAccess(); + stopForegroundService(); + assertBackgroundState(); + assertBackgroundNetworkAccess(false); + } + + @Test + public void testBackgroundNetworkAccess_whitelisted() throws Exception { + setDozeMode(true); + assertBackgroundNetworkAccess(false); + + addPowerSaveModeWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(true); + + removePowerSaveModeWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(false); + + addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(false); + + removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(false); + + assertsForegroundAlwaysHasNetworkAccess(); + assertBackgroundNetworkAccess(false); + } + + @Test + public void testBackgroundNetworkAccess_disabled() throws Exception { + assertBackgroundNetworkAccess(true); + + assertsForegroundAlwaysHasNetworkAccess(); + assertBackgroundNetworkAccess(true); + } + + @RequiredProperties({NOT_LOW_RAM_DEVICE}) + @Test + public void testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction() + throws Exception { + setPendingIntentWhitelistDuration(NETWORK_TIMEOUT_MS); + try { + registerNotificationListenerService(); + setDozeMode(true); + assertBackgroundNetworkAccess(false); + + testNotification(4, NOTIFICATION_TYPE_CONTENT); + testNotification(8, NOTIFICATION_TYPE_DELETE); + testNotification(15, NOTIFICATION_TYPE_FULL_SCREEN); + testNotification(16, NOTIFICATION_TYPE_BUNDLE); + testNotification(23, NOTIFICATION_TYPE_ACTION); + testNotification(42, NOTIFICATION_TYPE_ACTION_BUNDLE); + testNotification(108, NOTIFICATION_TYPE_ACTION_REMOTE_INPUT); + } finally { + resetDeviceIdleSettings(); + } + } + + private void testNotification(int id, String type) throws Exception { + sendNotification(id, type); + assertBackgroundNetworkAccess(true); + if (type.equals(NOTIFICATION_TYPE_ACTION)) { + // Make sure access is disabled after it expires. Since this check considerably slows + // downs the CTS tests, do it just once. + SystemClock.sleep(NETWORK_TIMEOUT_MS); + assertBackgroundNetworkAccess(false); + } + } + + // Must override so it only tests foreground service - once an app goes to foreground, device + // leaves Doze Mode. + @Override + protected void assertsForegroundAlwaysHasNetworkAccess() throws Exception { + launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE); + stopForegroundService(); + assertBackgroundState(); + } +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java new file mode 100644 index 0000000000..71f6f2f5f0 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java @@ -0,0 +1,880 @@ +/* + * 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.cts.net.hostside; + +import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED; +import static android.os.BatteryManager.BATTERY_PLUGGED_AC; +import static android.os.BatteryManager.BATTERY_PLUGGED_USB; +import static android.os.BatteryManager.BATTERY_PLUGGED_WIRELESS; + +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.executeShellCommand; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getConnectivityManager; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getContext; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getInstrumentation; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getWifiManager; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isDozeModeSupported; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.restrictBackgroundValueToString; + +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 android.app.ActivityManager; +import android.app.Instrumentation; +import android.app.NotificationManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.NetworkInfo.DetailedState; +import android.net.NetworkInfo.State; +import android.net.wifi.WifiManager; +import android.os.BatteryManager; +import android.os.Binder; +import android.os.Bundle; +import android.os.SystemClock; +import android.provider.Settings; +import android.service.notification.NotificationListenerService; +import android.util.Log; + +import org.junit.Rule; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * Superclass for tests related to background network restrictions. + */ +@RunWith(NetworkPolicyTestRunner.class) +public abstract class AbstractRestrictBackgroundNetworkTestCase { + public static final String TAG = "RestrictBackgroundNetworkTests"; + + protected static final String TEST_PKG = "com.android.cts.net.hostside"; + protected static final String TEST_APP2_PKG = "com.android.cts.net.hostside.app2"; + + private static final String TEST_APP2_ACTIVITY_CLASS = TEST_APP2_PKG + ".MyActivity"; + private static final String TEST_APP2_SERVICE_CLASS = TEST_APP2_PKG + ".MyForegroundService"; + + private static final int SLEEP_TIME_SEC = 1; + + // Constants below must match values defined on app2's Common.java + private static final String MANIFEST_RECEIVER = "ManifestReceiver"; + private static final String DYNAMIC_RECEIVER = "DynamicReceiver"; + + private static final String ACTION_RECEIVER_READY = + "com.android.cts.net.hostside.app2.action.RECEIVER_READY"; + static final String ACTION_SHOW_TOAST = + "com.android.cts.net.hostside.app2.action.SHOW_TOAST"; + + protected static final String NOTIFICATION_TYPE_CONTENT = "CONTENT"; + protected static final String NOTIFICATION_TYPE_DELETE = "DELETE"; + protected static final String NOTIFICATION_TYPE_FULL_SCREEN = "FULL_SCREEN"; + protected static final String NOTIFICATION_TYPE_BUNDLE = "BUNDLE"; + protected static final String NOTIFICATION_TYPE_ACTION = "ACTION"; + protected static final String NOTIFICATION_TYPE_ACTION_BUNDLE = "ACTION_BUNDLE"; + protected static final String NOTIFICATION_TYPE_ACTION_REMOTE_INPUT = "ACTION_REMOTE_INPUT"; + + // TODO: Update BatteryManager.BATTERY_PLUGGED_ANY as @TestApi + public static final int BATTERY_PLUGGED_ANY = + BATTERY_PLUGGED_AC | BATTERY_PLUGGED_USB | BATTERY_PLUGGED_WIRELESS; + + private static final String NETWORK_STATUS_SEPARATOR = "\\|"; + private static final int SECOND_IN_MS = 1000; + static final int NETWORK_TIMEOUT_MS = 15 * SECOND_IN_MS; + private static int PROCESS_STATE_FOREGROUND_SERVICE; + + private static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer"; + + protected static final int TYPE_COMPONENT_ACTIVTIY = 0; + protected static final int TYPE_COMPONENT_FOREGROUND_SERVICE = 1; + + private static final int BATTERY_STATE_TIMEOUT_MS = 5000; + private static final int BATTERY_STATE_CHECK_INTERVAL_MS = 500; + + private static final int FOREGROUND_PROC_NETWORK_TIMEOUT_MS = 6000; + + // Must be higher than NETWORK_TIMEOUT_MS + private static final int ORDERED_BROADCAST_TIMEOUT_MS = NETWORK_TIMEOUT_MS * 4; + + private static final IntentFilter BATTERY_CHANGED_FILTER = + new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + + private static final String APP_NOT_FOREGROUND_ERROR = "app_not_fg"; + + protected static final long TEMP_POWERSAVE_WHITELIST_DURATION_MS = 5_000; // 5 sec + + protected Context mContext; + protected Instrumentation mInstrumentation; + protected ConnectivityManager mCm; + protected int mUid; + private int mMyUid; + private MyServiceClient mServiceClient; + private String mDeviceIdleConstantsSetting; + + @Rule + public final RuleChain mRuleChain = RuleChain.outerRule(new RequiredPropertiesRule()) + .around(new MeterednessConfigurationRule()); + + protected void setUp() throws Exception { + + PROCESS_STATE_FOREGROUND_SERVICE = (Integer) ActivityManager.class + .getDeclaredField("PROCESS_STATE_FOREGROUND_SERVICE").get(null); + mInstrumentation = getInstrumentation(); + mContext = getContext(); + mCm = getConnectivityManager(); + mUid = getUid(TEST_APP2_PKG); + mMyUid = getUid(mContext.getPackageName()); + mServiceClient = new MyServiceClient(mContext); + mServiceClient.bind(); + mDeviceIdleConstantsSetting = "device_idle_constants"; + executeShellCommand("cmd netpolicy start-watching " + mUid); + setAppIdle(false); + + Log.i(TAG, "Apps status:\n" + + "\ttest app: uid=" + mMyUid + ", state=" + getProcessStateByUid(mMyUid) + "\n" + + "\tapp2: uid=" + mUid + ", state=" + getProcessStateByUid(mUid)); + } + + protected void tearDown() throws Exception { + executeShellCommand("cmd netpolicy stop-watching"); + mServiceClient.unbind(); + } + + protected int getUid(String packageName) throws Exception { + return mContext.getPackageManager().getPackageUid(packageName, 0); + } + + protected void assertRestrictBackgroundChangedReceived(int expectedCount) throws Exception { + assertRestrictBackgroundChangedReceived(DYNAMIC_RECEIVER, expectedCount); + assertRestrictBackgroundChangedReceived(MANIFEST_RECEIVER, 0); + } + + protected void assertRestrictBackgroundChangedReceived(String receiverName, int expectedCount) + throws Exception { + int attempts = 0; + int count = 0; + final int maxAttempts = 5; + do { + attempts++; + count = getNumberBroadcastsReceived(receiverName, ACTION_RESTRICT_BACKGROUND_CHANGED); + assertFalse("Expected count " + expectedCount + " but actual is " + count, + count > expectedCount); + if (count == expectedCount) { + break; + } + Log.d(TAG, "Expecting count " + expectedCount + " but actual is " + count + " after " + + attempts + " attempts; sleeping " + + SLEEP_TIME_SEC + " seconds before trying again"); + SystemClock.sleep(SLEEP_TIME_SEC * SECOND_IN_MS); + } while (attempts <= maxAttempts); + assertEquals("Number of expected broadcasts for " + receiverName + " not reached after " + + maxAttempts * SLEEP_TIME_SEC + " seconds", expectedCount, count); + } + + protected String sendOrderedBroadcast(Intent intent) throws Exception { + return sendOrderedBroadcast(intent, ORDERED_BROADCAST_TIMEOUT_MS); + } + + protected String sendOrderedBroadcast(Intent intent, int timeoutMs) throws Exception { + final LinkedBlockingQueue result = new LinkedBlockingQueue<>(1); + Log.d(TAG, "Sending ordered broadcast: " + intent); + mContext.sendOrderedBroadcast(intent, null, new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + final String resultData = getResultData(); + if (resultData == null) { + Log.e(TAG, "Received null data from ordered intent"); + return; + } + result.offer(resultData); + } + }, null, 0, null, null); + + final String resultData = result.poll(timeoutMs, TimeUnit.MILLISECONDS); + Log.d(TAG, "Ordered broadcast response after " + timeoutMs + "ms: " + resultData ); + return resultData; + } + + protected int getNumberBroadcastsReceived(String receiverName, String action) throws Exception { + return mServiceClient.getCounters(receiverName, action); + } + + protected void assertRestrictBackgroundStatus(int expectedStatus) throws Exception { + final String status = mServiceClient.getRestrictBackgroundStatus(); + assertNotNull("didn't get API status from app2", status); + assertEquals(restrictBackgroundValueToString(expectedStatus), + restrictBackgroundValueToString(Integer.parseInt(status))); + } + + protected void assertBackgroundNetworkAccess(boolean expectAllowed) throws Exception { + assertBackgroundState(); + assertNetworkAccess(expectAllowed /* expectAvailable */, false /* needScreenOn */); + } + + protected void assertForegroundNetworkAccess() throws Exception { + assertForegroundState(); + // We verified that app is in foreground state but if the screen turns-off while + // verifying for network access, the app will go into background state (in case app's + // foreground status was due to top activity). So, turn the screen on when verifying + // network connectivity. + assertNetworkAccess(true /* expectAvailable */, true /* needScreenOn */); + } + + protected void assertForegroundServiceNetworkAccess() throws Exception { + assertForegroundServiceState(); + assertNetworkAccess(true /* expectAvailable */, false /* needScreenOn */); + } + + /** + * Asserts that an app always have access while on foreground or running a foreground service. + * + *

This method will launch an activity and a foreground service to make the assertion, but + * will finish the activity / stop the service afterwards. + */ + protected void assertsForegroundAlwaysHasNetworkAccess() throws Exception{ + // Checks foreground first. + launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY); + finishActivity(); + + // Then foreground service + launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE); + stopForegroundService(); + } + + protected final void assertBackgroundState() throws Exception { + final int maxTries = 30; + ProcessState state = null; + for (int i = 1; i <= maxTries; i++) { + state = getProcessStateByUid(mUid); + Log.v(TAG, "assertBackgroundState(): status for app2 (" + mUid + ") on attempt #" + i + + ": " + state); + if (isBackground(state.state)) { + return; + } + Log.d(TAG, "App not on background state (" + state + ") on attempt #" + i + + "; sleeping 1s before trying again"); + SystemClock.sleep(SECOND_IN_MS); + } + fail("App2 is not on background state after " + maxTries + " attempts: " + state ); + } + + protected final void assertForegroundState() throws Exception { + final int maxTries = 30; + ProcessState state = null; + for (int i = 1; i <= maxTries; i++) { + state = getProcessStateByUid(mUid); + Log.v(TAG, "assertForegroundState(): status for app2 (" + mUid + ") on attempt #" + i + + ": " + state); + if (!isBackground(state.state)) { + return; + } + Log.d(TAG, "App not on foreground state on attempt #" + i + + "; sleeping 1s before trying again"); + turnScreenOn(); + SystemClock.sleep(SECOND_IN_MS); + } + fail("App2 is not on foreground state after " + maxTries + " attempts: " + state ); + } + + protected final void assertForegroundServiceState() throws Exception { + final int maxTries = 30; + ProcessState state = null; + for (int i = 1; i <= maxTries; i++) { + state = getProcessStateByUid(mUid); + Log.v(TAG, "assertForegroundServiceState(): status for app2 (" + mUid + ") on attempt #" + + i + ": " + state); + if (state.state == PROCESS_STATE_FOREGROUND_SERVICE) { + return; + } + Log.d(TAG, "App not on foreground service state on attempt #" + i + + "; sleeping 1s before trying again"); + SystemClock.sleep(SECOND_IN_MS); + } + fail("App2 is not on foreground service state after " + maxTries + " attempts: " + state ); + } + + /** + * Returns whether an app state should be considered "background" for restriction purposes. + */ + protected boolean isBackground(int state) { + return state > PROCESS_STATE_FOREGROUND_SERVICE; + } + + /** + * Asserts whether the active network is available or not. + */ + private void assertNetworkAccess(boolean expectAvailable, boolean needScreenOn) + throws Exception { + final int maxTries = 5; + String error = null; + int timeoutMs = 500; + + for (int i = 1; i <= maxTries; i++) { + error = checkNetworkAccess(expectAvailable); + + if (error.isEmpty()) return; + + // TODO: ideally, it should retry only when it cannot connect to an external site, + // or no retry at all! But, currently, the initial change fails almost always on + // battery saver tests because the netd changes are made asynchronously. + // Once b/27803922 is fixed, this retry mechanism should be revisited. + + Log.w(TAG, "Network status didn't match for expectAvailable=" + expectAvailable + + " on attempt #" + i + ": " + error + "\n" + + "Sleeping " + timeoutMs + "ms before trying again"); + if (needScreenOn) { + turnScreenOn(); + } + // No sleep after the last turn + if (i < maxTries) { + SystemClock.sleep(timeoutMs); + } + // Exponential back-off. + timeoutMs = Math.min(timeoutMs*2, NETWORK_TIMEOUT_MS); + } + fail("Invalid state for expectAvailable=" + expectAvailable + " after " + maxTries + + " attempts.\nLast error: " + error); + } + + /** + * Checks whether the network is available as expected. + * + * @return error message with the mismatch (or empty if assertion passed). + */ + private String checkNetworkAccess(boolean expectAvailable) throws Exception { + final String resultData = mServiceClient.checkNetworkStatus(); + return checkForAvailabilityInResultData(resultData, expectAvailable); + } + + private String checkForAvailabilityInResultData(String resultData, boolean expectAvailable) { + if (resultData == null) { + assertNotNull("Network status from app2 is null", resultData); + } + // Network status format is described on MyBroadcastReceiver.checkNetworkStatus() + final String[] parts = resultData.split(NETWORK_STATUS_SEPARATOR); + assertEquals("Wrong network status: " + resultData, 5, parts.length); + final State state = parts[0].equals("null") ? null : State.valueOf(parts[0]); + final DetailedState detailedState = parts[1].equals("null") + ? null : DetailedState.valueOf(parts[1]); + final boolean connected = Boolean.valueOf(parts[2]); + final String connectionCheckDetails = parts[3]; + final String networkInfo = parts[4]; + + final StringBuilder errors = new StringBuilder(); + final State expectedState; + final DetailedState expectedDetailedState; + if (expectAvailable) { + expectedState = State.CONNECTED; + expectedDetailedState = DetailedState.CONNECTED; + } else { + expectedState = State.DISCONNECTED; + expectedDetailedState = DetailedState.BLOCKED; + } + + if (expectAvailable != connected) { + errors.append(String.format("External site connection failed: expected %s, got %s\n", + expectAvailable, connected)); + } + if (expectedState != state || expectedDetailedState != detailedState) { + errors.append(String.format("Connection state mismatch: expected %s/%s, got %s/%s\n", + expectedState, expectedDetailedState, state, detailedState)); + } + + if (errors.length() > 0) { + errors.append("\tnetworkInfo: " + networkInfo + "\n"); + errors.append("\tconnectionCheckDetails: " + connectionCheckDetails + "\n"); + } + return errors.toString(); + } + + /** + * Runs a Shell command which is not expected to generate output. + */ + protected void executeSilentShellCommand(String command) { + final String result = executeShellCommand(command); + assertTrue("Command '" + command + "' failed: " + result, result.trim().isEmpty()); + } + + /** + * Asserts the result of a command, wait and re-running it a couple times if necessary. + */ + protected void assertDelayedShellCommand(String command, final String expectedResult) + throws Exception { + assertDelayedShellCommand(command, 5, 1, expectedResult); + } + + protected void assertDelayedShellCommand(String command, int maxTries, int napTimeSeconds, + final String expectedResult) throws Exception { + assertDelayedShellCommand(command, maxTries, napTimeSeconds, new ExpectResultChecker() { + + @Override + public boolean isExpected(String result) { + return expectedResult.equals(result); + } + + @Override + public String getExpected() { + return expectedResult; + } + }); + } + + protected void assertDelayedShellCommand(String command, int maxTries, int napTimeSeconds, + ExpectResultChecker checker) throws Exception { + String result = ""; + for (int i = 1; i <= maxTries; i++) { + result = executeShellCommand(command).trim(); + if (checker.isExpected(result)) return; + Log.v(TAG, "Command '" + command + "' returned '" + result + " instead of '" + + checker.getExpected() + "' on attempt #" + i + + "; sleeping " + napTimeSeconds + "s before trying again"); + SystemClock.sleep(napTimeSeconds * SECOND_IN_MS); + } + fail("Command '" + command + "' did not return '" + checker.getExpected() + "' after " + + maxTries + + " attempts. Last result: '" + result + "'"); + } + + protected void addRestrictBackgroundWhitelist(int uid) throws Exception { + executeShellCommand("cmd netpolicy add restrict-background-whitelist " + uid); + assertRestrictBackgroundWhitelist(uid, true); + // UID policies live by the Highlander rule: "There can be only one". + // Hence, if app is whitelisted, it should not be blacklisted. + assertRestrictBackgroundBlacklist(uid, false); + } + + protected void removeRestrictBackgroundWhitelist(int uid) throws Exception { + executeShellCommand("cmd netpolicy remove restrict-background-whitelist " + uid); + assertRestrictBackgroundWhitelist(uid, false); + } + + protected void assertRestrictBackgroundWhitelist(int uid, boolean expected) throws Exception { + assertRestrictBackground("restrict-background-whitelist", uid, expected); + } + + protected void addRestrictBackgroundBlacklist(int uid) throws Exception { + executeShellCommand("cmd netpolicy add restrict-background-blacklist " + uid); + assertRestrictBackgroundBlacklist(uid, true); + // UID policies live by the Highlander rule: "There can be only one". + // Hence, if app is blacklisted, it should not be whitelisted. + assertRestrictBackgroundWhitelist(uid, false); + } + + protected void removeRestrictBackgroundBlacklist(int uid) throws Exception { + executeShellCommand("cmd netpolicy remove restrict-background-blacklist " + uid); + assertRestrictBackgroundBlacklist(uid, false); + } + + protected void assertRestrictBackgroundBlacklist(int uid, boolean expected) throws Exception { + assertRestrictBackground("restrict-background-blacklist", uid, expected); + } + + protected void addAppIdleWhitelist(int uid) throws Exception { + executeShellCommand("cmd netpolicy add app-idle-whitelist " + uid); + assertAppIdleWhitelist(uid, true); + } + + protected void removeAppIdleWhitelist(int uid) throws Exception { + executeShellCommand("cmd netpolicy remove app-idle-whitelist " + uid); + assertAppIdleWhitelist(uid, false); + } + + protected void assertAppIdleWhitelist(int uid, boolean expected) throws Exception { + assertRestrictBackground("app-idle-whitelist", uid, expected); + } + + private void assertRestrictBackground(String list, int uid, boolean expected) throws Exception { + final int maxTries = 5; + boolean actual = false; + final String expectedUid = Integer.toString(uid); + String uids = ""; + for (int i = 1; i <= maxTries; i++) { + final String output = + executeShellCommand("cmd netpolicy list " + list); + uids = output.split(":")[1]; + for (String candidate : uids.split(" ")) { + actual = candidate.trim().equals(expectedUid); + if (expected == actual) { + return; + } + } + Log.v(TAG, list + " check for uid " + uid + " doesn't match yet (expected " + + expected + ", got " + actual + "); sleeping 1s before polling again"); + SystemClock.sleep(SECOND_IN_MS); + } + fail(list + " check for uid " + uid + " failed: expected " + expected + ", got " + actual + + ". Full list: " + uids); + } + + protected void addTempPowerSaveModeWhitelist(String packageName, long duration) + throws Exception { + Log.i(TAG, "Adding pkg " + packageName + " to temp-power-save-mode whitelist"); + executeShellCommand("dumpsys deviceidle tempwhitelist -d " + duration + " " + packageName); + } + + protected void assertPowerSaveModeWhitelist(String packageName, boolean expected) + throws Exception { + // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll + // need to use netpolicy for whitelisting + assertDelayedShellCommand("dumpsys deviceidle whitelist =" + packageName, + Boolean.toString(expected)); + } + + protected void addPowerSaveModeWhitelist(String packageName) throws Exception { + Log.i(TAG, "Adding package " + packageName + " to power-save-mode whitelist"); + // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll + // need to use netpolicy for whitelisting + executeShellCommand("dumpsys deviceidle whitelist +" + packageName); + assertPowerSaveModeWhitelist(packageName, true); + } + + protected void removePowerSaveModeWhitelist(String packageName) throws Exception { + Log.i(TAG, "Removing package " + packageName + " from power-save-mode whitelist"); + // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll + // need to use netpolicy for whitelisting + executeShellCommand("dumpsys deviceidle whitelist -" + packageName); + assertPowerSaveModeWhitelist(packageName, false); + } + + protected void assertPowerSaveModeExceptIdleWhitelist(String packageName, boolean expected) + throws Exception { + // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll + // need to use netpolicy for whitelisting + assertDelayedShellCommand("dumpsys deviceidle except-idle-whitelist =" + packageName, + Boolean.toString(expected)); + } + + protected void addPowerSaveModeExceptIdleWhitelist(String packageName) throws Exception { + Log.i(TAG, "Adding package " + packageName + " to power-save-mode-except-idle whitelist"); + // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll + // need to use netpolicy for whitelisting + executeShellCommand("dumpsys deviceidle except-idle-whitelist +" + packageName); + assertPowerSaveModeExceptIdleWhitelist(packageName, true); + } + + protected void removePowerSaveModeExceptIdleWhitelist(String packageName) throws Exception { + Log.i(TAG, "Removing package " + packageName + + " from power-save-mode-except-idle whitelist"); + // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll + // need to use netpolicy for whitelisting + executeShellCommand("dumpsys deviceidle except-idle-whitelist reset"); + assertPowerSaveModeExceptIdleWhitelist(packageName, false); + } + + protected void turnBatteryOn() throws Exception { + executeSilentShellCommand("cmd battery unplug"); + executeSilentShellCommand("cmd battery set status " + + BatteryManager.BATTERY_STATUS_DISCHARGING); + assertBatteryState(false); + } + + protected void turnBatteryOff() throws Exception { + executeSilentShellCommand("cmd battery set ac " + BATTERY_PLUGGED_ANY); + executeSilentShellCommand("cmd battery set level 100"); + executeSilentShellCommand("cmd battery set status " + + BatteryManager.BATTERY_STATUS_CHARGING); + assertBatteryState(true); + } + + private void assertBatteryState(boolean pluggedIn) throws Exception { + final long endTime = SystemClock.elapsedRealtime() + BATTERY_STATE_TIMEOUT_MS; + while (isDevicePluggedIn() != pluggedIn && SystemClock.elapsedRealtime() <= endTime) { + Thread.sleep(BATTERY_STATE_CHECK_INTERVAL_MS); + } + if (isDevicePluggedIn() != pluggedIn) { + fail("Timed out waiting for the plugged-in state to change," + + " expected pluggedIn: " + pluggedIn); + } + } + + private boolean isDevicePluggedIn() { + final Intent batteryIntent = mContext.registerReceiver(null, BATTERY_CHANGED_FILTER); + return batteryIntent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) > 0; + } + + protected void turnScreenOff() throws Exception { + executeSilentShellCommand("input keyevent KEYCODE_SLEEP"); + } + + protected void turnScreenOn() throws Exception { + executeSilentShellCommand("input keyevent KEYCODE_WAKEUP"); + executeSilentShellCommand("wm dismiss-keyguard"); + } + + protected void setBatterySaverMode(boolean enabled) throws Exception { + Log.i(TAG, "Setting Battery Saver Mode to " + enabled); + if (enabled) { + turnBatteryOn(); + executeSilentShellCommand("cmd power set-mode 1"); + } else { + executeSilentShellCommand("cmd power set-mode 0"); + turnBatteryOff(); + } + } + + protected void setDozeMode(boolean enabled) throws Exception { + // Check doze mode is supported. + assertTrue("Device does not support Doze Mode", isDozeModeSupported()); + + Log.i(TAG, "Setting Doze Mode to " + enabled); + if (enabled) { + turnBatteryOn(); + turnScreenOff(); + executeShellCommand("dumpsys deviceidle force-idle deep"); + } else { + turnScreenOn(); + turnBatteryOff(); + executeShellCommand("dumpsys deviceidle unforce"); + } + assertDozeMode(enabled); + } + + protected void assertDozeMode(boolean enabled) throws Exception { + assertDelayedShellCommand("dumpsys deviceidle get deep", enabled ? "IDLE" : "ACTIVE"); + } + + protected void setAppIdle(boolean enabled) throws Exception { + Log.i(TAG, "Setting app idle to " + enabled); + executeSilentShellCommand("am set-inactive " + TEST_APP2_PKG + " " + enabled ); + assertAppIdle(enabled); + } + + protected void setAppIdleNoAssert(boolean enabled) throws Exception { + Log.i(TAG, "Setting app idle to " + enabled); + executeSilentShellCommand("am set-inactive " + TEST_APP2_PKG + " " + enabled ); + } + + protected void assertAppIdle(boolean enabled) throws Exception { + try { + assertDelayedShellCommand("am get-inactive " + TEST_APP2_PKG, 15, 2, "Idle=" + enabled); + } catch (Throwable e) { + throw e; + } + } + + /** + * Starts a service that will register a broadcast receiver to receive + * {@code RESTRICT_BACKGROUND_CHANGE} intents. + *

+ * The service must run in a separate app because otherwise it would be killed every time + * {@link #runDeviceTests(String, String)} is executed. + */ + protected void registerBroadcastReceiver() throws Exception { + mServiceClient.registerBroadcastReceiver(); + + final Intent intent = new Intent(ACTION_RECEIVER_READY) + .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + // Wait until receiver is ready. + final int maxTries = 10; + for (int i = 1; i <= maxTries; i++) { + final String message = sendOrderedBroadcast(intent, SECOND_IN_MS * 4); + Log.d(TAG, "app2 receiver acked: " + message); + if (message != null) { + return; + } + Log.v(TAG, "app2 receiver is not ready yet; sleeping 1s before polling again"); + SystemClock.sleep(SECOND_IN_MS); + } + fail("app2 receiver is not ready"); + } + + protected void registerNetworkCallback(INetworkCallback cb) throws Exception { + mServiceClient.registerNetworkCallback(cb); + } + + protected void unregisterNetworkCallback() throws Exception { + mServiceClient.unregisterNetworkCallback(); + } + + /** + * Registers a {@link NotificationListenerService} implementation that will execute the + * notification actions right after the notification is sent. + */ + protected void registerNotificationListenerService() throws Exception { + executeShellCommand("cmd notification allow_listener " + + MyNotificationListenerService.getId()); + final NotificationManager nm = mContext.getSystemService(NotificationManager.class); + final ComponentName listenerComponent = MyNotificationListenerService.getComponentName(); + assertTrue(listenerComponent + " has not been granted access", + nm.isNotificationListenerAccessGranted(listenerComponent)); + } + + protected void setPendingIntentWhitelistDuration(int durationMs) throws Exception { + executeSilentShellCommand(String.format( + "settings put global %s %s=%d", mDeviceIdleConstantsSetting, + "notification_whitelist_duration", durationMs)); + } + + protected void resetDeviceIdleSettings() throws Exception { + executeShellCommand(String.format("settings delete global %s", + mDeviceIdleConstantsSetting)); + } + + protected void launchComponentAndAssertNetworkAccess(int type) throws Exception { + if (type == TYPE_COMPONENT_FOREGROUND_SERVICE) { + startForegroundService(); + assertForegroundServiceNetworkAccess(); + return; + } else if (type == TYPE_COMPONENT_ACTIVTIY) { + turnScreenOn(); + // Wait for screen-on state to propagate through the system. + SystemClock.sleep(2000); + final CountDownLatch latch = new CountDownLatch(1); + final Intent launchIntent = getIntentForComponent(type); + final Bundle extras = new Bundle(); + final String[] errors = new String[]{null}; + extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, errors)); + launchIntent.putExtras(extras); + mContext.startActivity(launchIntent); + if (latch.await(FOREGROUND_PROC_NETWORK_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + if (!errors[0].isEmpty()) { + if (errors[0] == APP_NOT_FOREGROUND_ERROR) { + // App didn't come to foreground when the activity is started, so try again. + assertForegroundNetworkAccess(); + } else { + fail("Network is not available for app2 (" + mUid + "): " + errors[0]); + } + } + } else { + fail("Timed out waiting for network availability status from app2 (" + mUid + ")"); + } + } else { + throw new IllegalArgumentException("Unknown type: " + type); + } + } + + private void startForegroundService() throws Exception { + final Intent launchIntent = getIntentForComponent(TYPE_COMPONENT_FOREGROUND_SERVICE); + mContext.startForegroundService(launchIntent); + assertForegroundServiceState(); + } + + private Intent getIntentForComponent(int type) { + final Intent intent = new Intent(); + if (type == TYPE_COMPONENT_ACTIVTIY) { + intent.setComponent(new ComponentName(TEST_APP2_PKG, TEST_APP2_ACTIVITY_CLASS)) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } else if (type == TYPE_COMPONENT_FOREGROUND_SERVICE) { + intent.setComponent(new ComponentName(TEST_APP2_PKG, TEST_APP2_SERVICE_CLASS)) + .setFlags(1); + } else { + fail("Unknown type: " + type); + } + return intent; + } + + protected void stopForegroundService() throws Exception { + executeShellCommand(String.format("am startservice -f 2 %s/%s", + TEST_APP2_PKG, TEST_APP2_SERVICE_CLASS)); + // NOTE: cannot assert state because it depends on whether activity was on top before. + } + + private Binder getNewNetworkStateObserver(final CountDownLatch latch, + final String[] errors) { + return new INetworkStateObserver.Stub() { + @Override + public boolean isForeground() { + try { + final ProcessState state = getProcessStateByUid(mUid); + return !isBackground(state.state); + } catch (Exception e) { + Log.d(TAG, "Error while reading the proc state for " + mUid + ": " + e); + return false; + } + } + + @Override + public void onNetworkStateChecked(String resultData) { + errors[0] = resultData == null + ? APP_NOT_FOREGROUND_ERROR + : checkForAvailabilityInResultData(resultData, true); + latch.countDown(); + } + }; + } + + /** + * Finishes an activity on app2 so its process is demoted fromforeground status. + */ + protected void finishActivity() throws Exception { + executeShellCommand("am broadcast -a " + + " com.android.cts.net.hostside.app2.action.FINISH_ACTIVITY " + + "--receiver-foreground --receiver-registered-only"); + } + + protected void sendNotification(int notificationId, String notificationType) throws Exception { + Log.d(TAG, "Sending notification broadcast (id=" + notificationId + + ", type=" + notificationType); + mServiceClient.sendNotification(notificationId, notificationType); + } + + protected String showToast() { + final Intent intent = new Intent(ACTION_SHOW_TOAST); + intent.setPackage(TEST_APP2_PKG); + Log.d(TAG, "Sending request to show toast"); + try { + return sendOrderedBroadcast(intent, 3 * SECOND_IN_MS); + } catch (Exception e) { + return ""; + } + } + + private ProcessState getProcessStateByUid(int uid) throws Exception { + return new ProcessState(executeShellCommand("cmd activity get-uid-state " + uid)); + } + + private static class ProcessState { + private final String fullState; + final int state; + + ProcessState(String fullState) { + this.fullState = fullState; + try { + this.state = Integer.parseInt(fullState.split(" ")[0]); + } catch (Exception e) { + throw new IllegalArgumentException("Could not parse " + fullState); + } + } + + @Override + public String toString() { + return fullState; + } + } + + /** + * Helper class used to assert the result of a Shell command. + */ + protected static interface ExpectResultChecker { + /** + * Checkes whether the result of the command matched the expectation. + */ + boolean isExpected(String result); + /** + * Gets the expected result so it's displayed on log and failure messages. + */ + String getExpected(); + } +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleMeteredTest.java new file mode 100644 index 0000000000..f1858d65a5 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleMeteredTest.java @@ -0,0 +1,23 @@ +/* + * 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.cts.net.hostside; + +import static com.android.cts.net.hostside.Property.METERED_NETWORK; + +@RequiredProperties({METERED_NETWORK}) +public class AppIdleMeteredTest extends AbstractAppIdleTestCase { +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleNonMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleNonMeteredTest.java new file mode 100644 index 0000000000..e737a6dabe --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleNonMeteredTest.java @@ -0,0 +1,23 @@ +/* + * 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.cts.net.hostside; + +import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK; + +@RequiredProperties({NON_METERED_NETWORK}) +public class AppIdleNonMeteredTest extends AbstractAppIdleTestCase { +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeMeteredTest.java new file mode 100644 index 0000000000..c78ca2ec77 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeMeteredTest.java @@ -0,0 +1,23 @@ +/* + * 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.cts.net.hostside; + +import static com.android.cts.net.hostside.Property.METERED_NETWORK; + +@RequiredProperties({METERED_NETWORK}) +public class BatterySaverModeMeteredTest extends AbstractBatterySaverModeTestCase { +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeNonMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeNonMeteredTest.java new file mode 100644 index 0000000000..fb52a540d8 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeNonMeteredTest.java @@ -0,0 +1,24 @@ +/* + * 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.cts.net.hostside; + + +import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK; + +@RequiredProperties({NON_METERED_NETWORK}) +public class BatterySaverModeNonMeteredTest extends AbstractBatterySaverModeTestCase { +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java new file mode 100644 index 0000000000..604a0b62c6 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java @@ -0,0 +1,207 @@ +/* + * 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.cts.net.hostside; + +import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED; +import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED; +import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED; + +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground; +import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE; +import static com.android.cts.net.hostside.Property.METERED_NETWORK; +import static com.android.cts.net.hostside.Property.NO_DATA_SAVER_MODE; + +import static org.junit.Assert.fail; + +import com.android.compatibility.common.util.CddTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import androidx.test.filters.LargeTest; + +@RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK}) +@LargeTest +public class DataSaverModeTest extends AbstractRestrictBackgroundNetworkTestCase { + + private static final String[] REQUIRED_WHITELISTED_PACKAGES = { + "com.android.providers.downloads" + }; + + @Before + public void setUp() throws Exception { + super.setUp(); + + // Set initial state. + setRestrictBackground(false); + removeRestrictBackgroundWhitelist(mUid); + removeRestrictBackgroundBlacklist(mUid); + + registerBroadcastReceiver(); + assertRestrictBackgroundChangedReceived(0); + } + + @After + public void tearDown() throws Exception { + super.tearDown(); + + setRestrictBackground(false); + } + + @Test + public void testGetRestrictBackgroundStatus_disabled() throws Exception { + assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED); + + // Verify status is always disabled, never whitelisted + addRestrictBackgroundWhitelist(mUid); + assertRestrictBackgroundChangedReceived(0); + assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED); + + assertsForegroundAlwaysHasNetworkAccess(); + assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED); + } + + @Test + public void testGetRestrictBackgroundStatus_whitelisted() throws Exception { + setRestrictBackground(true); + assertRestrictBackgroundChangedReceived(1); + assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED); + + addRestrictBackgroundWhitelist(mUid); + assertRestrictBackgroundChangedReceived(2); + assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_WHITELISTED); + + removeRestrictBackgroundWhitelist(mUid); + assertRestrictBackgroundChangedReceived(3); + assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED); + + assertsForegroundAlwaysHasNetworkAccess(); + assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED); + } + + @Test + public void testGetRestrictBackgroundStatus_enabled() throws Exception { + setRestrictBackground(true); + assertRestrictBackgroundChangedReceived(1); + assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED); + + assertsForegroundAlwaysHasNetworkAccess(); + assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED); + + // Make sure foreground app doesn't lose access upon enabling Data Saver. + setRestrictBackground(false); + launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY); + setRestrictBackground(true); + assertForegroundNetworkAccess(); + + // Although it should not have access while the screen is off. + turnScreenOff(); + assertBackgroundNetworkAccess(false); + turnScreenOn(); + assertForegroundNetworkAccess(); + + // Goes back to background state. + finishActivity(); + assertBackgroundNetworkAccess(false); + + // Make sure foreground service doesn't lose access upon enabling Data Saver. + setRestrictBackground(false); + launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE); + setRestrictBackground(true); + assertForegroundNetworkAccess(); + stopForegroundService(); + assertBackgroundNetworkAccess(false); + } + + @Test + public void testGetRestrictBackgroundStatus_blacklisted() throws Exception { + addRestrictBackgroundBlacklist(mUid); + assertRestrictBackgroundChangedReceived(1); + assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED); + + assertsForegroundAlwaysHasNetworkAccess(); + assertRestrictBackgroundChangedReceived(1); + assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED); + + // UID policies live by the Highlander rule: "There can be only one". + // Hence, if app is whitelisted, it should not be blacklisted anymore. + setRestrictBackground(true); + assertRestrictBackgroundChangedReceived(2); + assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED); + addRestrictBackgroundWhitelist(mUid); + assertRestrictBackgroundChangedReceived(3); + assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_WHITELISTED); + + // Check status after removing blacklist. + // ...re-enables first + addRestrictBackgroundBlacklist(mUid); + assertRestrictBackgroundChangedReceived(4); + assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED); + assertsForegroundAlwaysHasNetworkAccess(); + // ... remove blacklist - access's still rejected because Data Saver is on + removeRestrictBackgroundBlacklist(mUid); + assertRestrictBackgroundChangedReceived(4); + assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED); + assertsForegroundAlwaysHasNetworkAccess(); + // ... finally, disable Data Saver + setRestrictBackground(false); + assertRestrictBackgroundChangedReceived(5); + assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED); + assertsForegroundAlwaysHasNetworkAccess(); + } + + @Test + public void testGetRestrictBackgroundStatus_requiredWhitelistedPackages() throws Exception { + final StringBuilder error = new StringBuilder(); + for (String packageName : REQUIRED_WHITELISTED_PACKAGES) { + int uid = -1; + try { + uid = getUid(packageName); + assertRestrictBackgroundWhitelist(uid, true); + } catch (Throwable t) { + error.append("\nFailed for '").append(packageName).append("'"); + if (uid > 0) { + error.append(" (uid ").append(uid).append(")"); + } + error.append(": ").append(t).append("\n"); + } + } + if (error.length() > 0) { + fail(error.toString()); + } + } + + @RequiredProperties({NO_DATA_SAVER_MODE}) + @CddTest(requirement="7.4.7/C-2-2") + @Test + public void testBroadcastNotSentOnUnsupportedDevices() throws Exception { + setRestrictBackground(true); + assertRestrictBackgroundChangedReceived(0); + + setRestrictBackground(false); + assertRestrictBackgroundChangedReceived(0); + + setRestrictBackground(true); + assertRestrictBackgroundChangedReceived(0); + } + + private void assertDataSaverStatusOnBackground(int expectedStatus) throws Exception { + assertRestrictBackgroundStatus(expectedStatus); + assertBackgroundNetworkAccess(expectedStatus != RESTRICT_BACKGROUND_STATUS_ENABLED); + } +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeMeteredTest.java new file mode 100644 index 0000000000..4306c991c2 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeMeteredTest.java @@ -0,0 +1,23 @@ +/* + * 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.cts.net.hostside; + +import static com.android.cts.net.hostside.Property.METERED_NETWORK; + +@RequiredProperties({METERED_NETWORK}) +public class DozeModeMeteredTest extends AbstractDozeModeTestCase { +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeNonMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeNonMeteredTest.java new file mode 100644 index 0000000000..1e89f158a3 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeNonMeteredTest.java @@ -0,0 +1,23 @@ +/* + * 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.cts.net.hostside; + +import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK; + +@RequiredProperties({NON_METERED_NETWORK}) +public class DozeModeNonMeteredTest extends AbstractDozeModeTestCase { +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java new file mode 100644 index 0000000000..5ecb399da0 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java @@ -0,0 +1,92 @@ +/* + * 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.cts.net.hostside; + +import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG; +import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TEST_APP2_PKG; +import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TEST_PKG; + +import android.os.Environment; +import android.os.FileUtils; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import com.android.compatibility.common.util.OnFailureRule; + +import org.junit.AssumptionViolatedException; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import androidx.test.platform.app.InstrumentationRegistry; + +public class DumpOnFailureRule extends OnFailureRule { + private File mDumpDir = new File(Environment.getExternalStorageDirectory(), + "CtsHostsideNetworkTests"); + + @Override + public void onTestFailure(Statement base, Description description, Throwable throwable) { + final String testName = description.getClassName() + "_" + description.getMethodName(); + + if (throwable instanceof AssumptionViolatedException) { + Log.d(TAG, "Skipping test " + testName + ": " + throwable); + return; + } + + prepareDumpRootDir(); + final File dumpFile = new File(mDumpDir, "dump-" + testName); + Log.i(TAG, "Dumping debug info for " + description + ": " + dumpFile.getPath()); + try (FileOutputStream out = new FileOutputStream(dumpFile)) { + for (String cmd : new String[] { + "dumpsys netpolicy", + "dumpsys network_management", + "dumpsys usagestats " + TEST_PKG + " " + TEST_APP2_PKG, + "dumpsys usagestats appstandby", + }) { + dumpCommandOutput(out, cmd); + } + } catch (FileNotFoundException e) { + Log.e(TAG, "Error opening file: " + dumpFile, e); + } catch (IOException e) { + Log.e(TAG, "Error closing file: " + dumpFile, e); + } + } + + void dumpCommandOutput(FileOutputStream out, String cmd) { + final ParcelFileDescriptor pfd = InstrumentationRegistry.getInstrumentation() + .getUiAutomation().executeShellCommand(cmd); + try (FileInputStream in = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) { + out.write(("Output of '" + cmd + "':\n").getBytes(StandardCharsets.UTF_8)); + FileUtils.copy(in, out); + out.write("\n\n=================================================================\n\n" + .getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + Log.e(TAG, "Error dumping '" + cmd + "'", e); + } + } + + void prepareDumpRootDir() { + if (!mDumpDir.exists() && !mDumpDir.mkdir()) { + Log.e(TAG, "Error creating " + mDumpDir); + } + } +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MeterednessConfigurationRule.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MeterednessConfigurationRule.java new file mode 100644 index 0000000000..8fadf9e295 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MeterednessConfigurationRule.java @@ -0,0 +1,60 @@ +/* + * 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.cts.net.hostside; + +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.resetMeteredNetwork; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setupMeteredNetwork; +import static com.android.cts.net.hostside.Property.METERED_NETWORK; +import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK; + +import android.util.ArraySet; +import android.util.Pair; + +import com.android.compatibility.common.util.BeforeAfterRule; + +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +public class MeterednessConfigurationRule extends BeforeAfterRule { + private Pair mSsidAndInitialMeteredness; + + @Override + public void onBefore(Statement base, Description description) throws Throwable { + final ArraySet requiredProperties + = RequiredPropertiesRule.getRequiredProperties(); + if (requiredProperties.contains(METERED_NETWORK)) { + configureNetworkMeteredness(true); + } else if (requiredProperties.contains(NON_METERED_NETWORK)) { + configureNetworkMeteredness(false); + } + } + + @Override + public void onAfter(Statement base, Description description) throws Throwable { + resetNetworkMeteredness(); + } + + public void configureNetworkMeteredness(boolean metered) throws Exception { + mSsidAndInitialMeteredness = setupMeteredNetwork(metered); + } + + public void resetNetworkMeteredness() throws Exception { + if (mSsidAndInitialMeteredness != null) { + resetMeteredNetwork(mSsidAndInitialMeteredness.first, + mSsidAndInitialMeteredness.second); + } + } +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MixedModesTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MixedModesTest.java new file mode 100644 index 0000000000..c9edda6e0b --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MixedModesTest.java @@ -0,0 +1,370 @@ +/* + * 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.cts.net.hostside; + +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground; +import static com.android.cts.net.hostside.Property.APP_STANDBY_MODE; +import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE; +import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE; +import static com.android.cts.net.hostside.Property.DOZE_MODE; +import static com.android.cts.net.hostside.Property.METERED_NETWORK; +import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK; + +import android.os.SystemClock; +import android.util.Log; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Test cases for the more complex scenarios where multiple restrictions (like Battery Saver Mode + * and Data Saver Mode) are applied simultaneously. + *

+ * NOTE: it might sound like the test methods on this class are testing too much, + * which would make it harder to diagnose individual failures, but the assumption is that such + * failure most likely will happen when the restriction is tested individually as well. + */ +public class MixedModesTest extends AbstractRestrictBackgroundNetworkTestCase { + private static final String TAG = "MixedModesTest"; + + @Before + public void setUp() throws Exception { + super.setUp(); + + // Set initial state. + removeRestrictBackgroundWhitelist(mUid); + removeRestrictBackgroundBlacklist(mUid); + removePowerSaveModeWhitelist(TEST_APP2_PKG); + removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG); + + registerBroadcastReceiver(); + } + + @After + public void tearDown() throws Exception { + super.tearDown(); + + try { + setRestrictBackground(false); + } finally { + setBatterySaverMode(false); + } + } + + /** + * Tests all DS ON and BS ON scenarios from network-policy-restrictions.md on metered networks. + */ + @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, METERED_NETWORK}) + @Test + public void testDataAndBatterySaverModes_meteredNetwork() throws Exception { + final MeterednessConfigurationRule meterednessConfiguration + = new MeterednessConfigurationRule(); + meterednessConfiguration.configureNetworkMeteredness(true); + try { + setRestrictBackground(true); + setBatterySaverMode(true); + + Log.v(TAG, "Not whitelisted for any."); + assertBackgroundNetworkAccess(false); + assertsForegroundAlwaysHasNetworkAccess(); + assertBackgroundNetworkAccess(false); + + Log.v(TAG, "Whitelisted for Data Saver but not for Battery Saver."); + addRestrictBackgroundWhitelist(mUid); + removePowerSaveModeWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(false); + assertsForegroundAlwaysHasNetworkAccess(); + assertBackgroundNetworkAccess(false); + removeRestrictBackgroundWhitelist(mUid); + + Log.v(TAG, "Whitelisted for Battery Saver but not for Data Saver."); + addPowerSaveModeWhitelist(TEST_APP2_PKG); + removeRestrictBackgroundWhitelist(mUid); + assertBackgroundNetworkAccess(false); + assertsForegroundAlwaysHasNetworkAccess(); + assertBackgroundNetworkAccess(false); + removePowerSaveModeWhitelist(TEST_APP2_PKG); + + Log.v(TAG, "Whitelisted for both."); + addRestrictBackgroundWhitelist(mUid); + addPowerSaveModeWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(true); + assertsForegroundAlwaysHasNetworkAccess(); + assertBackgroundNetworkAccess(true); + removePowerSaveModeWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(false); + removeRestrictBackgroundWhitelist(mUid); + + Log.v(TAG, "Blacklisted for Data Saver, not whitelisted for Battery Saver."); + addRestrictBackgroundBlacklist(mUid); + removePowerSaveModeWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(false); + assertsForegroundAlwaysHasNetworkAccess(); + assertBackgroundNetworkAccess(false); + removeRestrictBackgroundBlacklist(mUid); + + Log.v(TAG, "Blacklisted for Data Saver, whitelisted for Battery Saver."); + addRestrictBackgroundBlacklist(mUid); + addPowerSaveModeWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(false); + assertsForegroundAlwaysHasNetworkAccess(); + assertBackgroundNetworkAccess(false); + removeRestrictBackgroundBlacklist(mUid); + removePowerSaveModeWhitelist(TEST_APP2_PKG); + } finally { + meterednessConfiguration.resetNetworkMeteredness(); + } + } + + /** + * Tests all DS ON and BS ON scenarios from network-policy-restrictions.md on non-metered + * networks. + */ + @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, NON_METERED_NETWORK}) + @Test + public void testDataAndBatterySaverModes_nonMeteredNetwork() throws Exception { + final MeterednessConfigurationRule meterednessConfiguration + = new MeterednessConfigurationRule(); + meterednessConfiguration.configureNetworkMeteredness(false); + try { + setRestrictBackground(true); + setBatterySaverMode(true); + + Log.v(TAG, "Not whitelisted for any."); + assertBackgroundNetworkAccess(false); + assertsForegroundAlwaysHasNetworkAccess(); + assertBackgroundNetworkAccess(false); + + Log.v(TAG, "Whitelisted for Data Saver but not for Battery Saver."); + addRestrictBackgroundWhitelist(mUid); + removePowerSaveModeWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(false); + assertsForegroundAlwaysHasNetworkAccess(); + assertBackgroundNetworkAccess(false); + removeRestrictBackgroundWhitelist(mUid); + + Log.v(TAG, "Whitelisted for Battery Saver but not for Data Saver."); + addPowerSaveModeWhitelist(TEST_APP2_PKG); + removeRestrictBackgroundWhitelist(mUid); + assertBackgroundNetworkAccess(true); + assertsForegroundAlwaysHasNetworkAccess(); + assertBackgroundNetworkAccess(true); + removePowerSaveModeWhitelist(TEST_APP2_PKG); + + Log.v(TAG, "Whitelisted for both."); + addRestrictBackgroundWhitelist(mUid); + addPowerSaveModeWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(true); + assertsForegroundAlwaysHasNetworkAccess(); + assertBackgroundNetworkAccess(true); + removePowerSaveModeWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(false); + removeRestrictBackgroundWhitelist(mUid); + + Log.v(TAG, "Blacklisted for Data Saver, not whitelisted for Battery Saver."); + addRestrictBackgroundBlacklist(mUid); + removePowerSaveModeWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(false); + assertsForegroundAlwaysHasNetworkAccess(); + assertBackgroundNetworkAccess(false); + removeRestrictBackgroundBlacklist(mUid); + + Log.v(TAG, "Blacklisted for Data Saver, whitelisted for Battery Saver."); + addRestrictBackgroundBlacklist(mUid); + addPowerSaveModeWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(true); + assertsForegroundAlwaysHasNetworkAccess(); + assertBackgroundNetworkAccess(true); + removeRestrictBackgroundBlacklist(mUid); + removePowerSaveModeWhitelist(TEST_APP2_PKG); + } finally { + meterednessConfiguration.resetNetworkMeteredness(); + } + } + + /** + * Tests that powersave whitelists works as expected when doze and battery saver modes + * are enabled. + */ + @RequiredProperties({DOZE_MODE, BATTERY_SAVER_MODE}) + @Test + public void testDozeAndBatterySaverMode_powerSaveWhitelists() throws Exception { + setBatterySaverMode(true); + setDozeMode(true); + + try { + addPowerSaveModeWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(true); + + removePowerSaveModeWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(false); + + addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(false); + + removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(false); + } finally { + setBatterySaverMode(false); + setDozeMode(false); + } + } + + /** + * Tests that powersave whitelists works as expected when doze and appIdle modes + * are enabled. + */ + @RequiredProperties({DOZE_MODE, APP_STANDBY_MODE}) + @Test + public void testDozeAndAppIdle_powerSaveWhitelists() throws Exception { + setDozeMode(true); + setAppIdle(true); + + try { + addPowerSaveModeWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(true); + + removePowerSaveModeWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(false); + + addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(false); + + removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG); + assertBackgroundNetworkAccess(false); + } finally { + setAppIdle(false); + setDozeMode(false); + } + } + + @RequiredProperties({APP_STANDBY_MODE, DOZE_MODE}) + @Test + public void testAppIdleAndDoze_tempPowerSaveWhitelists() throws Exception { + setDozeMode(true); + setAppIdle(true); + + try { + assertBackgroundNetworkAccess(false); + + addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS); + assertBackgroundNetworkAccess(true); + + // Wait until the whitelist duration is expired. + SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS); + assertBackgroundNetworkAccess(false); + } finally { + setAppIdle(false); + setDozeMode(false); + } + } + + @RequiredProperties({APP_STANDBY_MODE, BATTERY_SAVER_MODE}) + @Test + public void testAppIdleAndBatterySaver_tempPowerSaveWhitelists() throws Exception { + setBatterySaverMode(true); + setAppIdle(true); + + try { + assertBackgroundNetworkAccess(false); + + addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS); + assertBackgroundNetworkAccess(true); + + // Wait until the whitelist duration is expired. + SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS); + assertBackgroundNetworkAccess(false); + } finally { + setAppIdle(false); + setBatterySaverMode(false); + } + } + + /** + * Tests that the app idle whitelist works as expected when doze and appIdle mode are enabled. + */ + @RequiredProperties({DOZE_MODE, APP_STANDBY_MODE}) + @Test + public void testDozeAndAppIdle_appIdleWhitelist() throws Exception { + setDozeMode(true); + setAppIdle(true); + + try { + assertBackgroundNetworkAccess(false); + + // UID still shouldn't have access because of Doze. + addAppIdleWhitelist(mUid); + assertBackgroundNetworkAccess(false); + + removeAppIdleWhitelist(mUid); + assertBackgroundNetworkAccess(false); + } finally { + setAppIdle(false); + setDozeMode(false); + } + } + + @RequiredProperties({APP_STANDBY_MODE, DOZE_MODE}) + @Test + public void testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists() throws Exception { + setDozeMode(true); + setAppIdle(true); + + try { + assertBackgroundNetworkAccess(false); + + addAppIdleWhitelist(mUid); + assertBackgroundNetworkAccess(false); + + addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS); + assertBackgroundNetworkAccess(true); + + // Wait until the whitelist duration is expired. + SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS); + assertBackgroundNetworkAccess(false); + } finally { + setAppIdle(false); + setDozeMode(false); + removeAppIdleWhitelist(mUid); + } + } + + @RequiredProperties({APP_STANDBY_MODE, BATTERY_SAVER_MODE}) + @Test + public void testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists() throws Exception { + setBatterySaverMode(true); + setAppIdle(true); + + try { + assertBackgroundNetworkAccess(false); + + addAppIdleWhitelist(mUid); + assertBackgroundNetworkAccess(false); + + addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS); + assertBackgroundNetworkAccess(true); + + // Wait until the whitelist duration is expired. + SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS); + assertBackgroundNetworkAccess(false); + } finally { + setAppIdle(false); + setBatterySaverMode(false); + removeAppIdleWhitelist(mUid); + } + } +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyActivity.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyActivity.java new file mode 100644 index 0000000000..0d0bc58504 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyActivity.java @@ -0,0 +1,49 @@ +/* + * 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 com.android.cts.net.hostside; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.WindowManager; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +public class MyActivity extends Activity { + private final LinkedBlockingQueue mResult = new LinkedBlockingQueue<>(1); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (mResult.offer(resultCode) == false) { + throw new RuntimeException("Queue is full! This should never happen"); + } + } + + public Integer getResult(int timeoutMs) throws InterruptedException { + return mResult.poll(timeoutMs, TimeUnit.MILLISECONDS); + } +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyNotificationListenerService.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyNotificationListenerService.java new file mode 100644 index 0000000000..013253670a --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyNotificationListenerService.java @@ -0,0 +1,123 @@ +/* + * 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.cts.net.hostside; + +import android.app.Notification; +import android.app.PendingIntent; +import android.app.PendingIntent.CanceledException; +import android.app.RemoteInput; +import android.content.ComponentName; +import android.os.Bundle; +import android.service.notification.NotificationListenerService; +import android.service.notification.StatusBarNotification; +import android.util.Log; + +/** + * NotificationListenerService implementation that executes the notification actions once they're + * created. + */ +public class MyNotificationListenerService extends NotificationListenerService { + private static final String TAG = "MyNotificationListenerService"; + + @Override + public void onListenerConnected() { + Log.d(TAG, "onListenerConnected()"); + } + + @Override + public void onNotificationPosted(StatusBarNotification sbn) { + Log.d(TAG, "onNotificationPosted(): " + sbn); + if (!sbn.getPackageName().startsWith(getPackageName())) { + Log.v(TAG, "ignoring notification from a different package"); + return; + } + final PendingIntentSender sender = new PendingIntentSender(); + final Notification notification = sbn.getNotification(); + if (notification.contentIntent != null) { + sender.send("content", notification.contentIntent); + } + if (notification.deleteIntent != null) { + sender.send("delete", notification.deleteIntent); + } + if (notification.fullScreenIntent != null) { + sender.send("full screen", notification.fullScreenIntent); + } + if (notification.actions != null) { + for (Notification.Action action : notification.actions) { + sender.send("action", action.actionIntent); + sender.send("action extras", action.getExtras()); + final RemoteInput[] remoteInputs = action.getRemoteInputs(); + if (remoteInputs != null && remoteInputs.length > 0) { + for (RemoteInput remoteInput : remoteInputs) { + sender.send("remote input extras", remoteInput.getExtras()); + } + } + } + } + sender.send("notification extras", notification.extras); + } + + static String getId() { + return String.format("%s/%s", MyNotificationListenerService.class.getPackage().getName(), + MyNotificationListenerService.class.getName()); + } + + static ComponentName getComponentName() { + return new ComponentName(MyNotificationListenerService.class.getPackage().getName(), + MyNotificationListenerService.class.getName()); + } + + private static final class PendingIntentSender { + private PendingIntent mSentIntent = null; + private String mReason = null; + + private void send(String reason, PendingIntent pendingIntent) { + if (pendingIntent == null) { + // Could happen on action that only has extras + Log.v(TAG, "Not sending null pending intent for " + reason); + return; + } + if (mSentIntent != null || mReason != null) { + // Sanity check: make sure test case set up just one pending intent in the + // notification, otherwise it could pass because another pending intent caused the + // whitelisting. + throw new IllegalStateException("Already sent a PendingIntent (" + mSentIntent + + ") for reason '" + mReason + "' when requested another for '" + reason + + "' (" + pendingIntent + ")"); + } + Log.i(TAG, "Sending pending intent for " + reason + ":" + pendingIntent); + try { + pendingIntent.send(); + mSentIntent = pendingIntent; + mReason = reason; + } catch (CanceledException e) { + Log.w(TAG, "Pending intent " + pendingIntent + " canceled"); + } + } + + private void send(String reason, Bundle extras) { + if (extras != null) { + for (String key : extras.keySet()) { + Object value = extras.get(key); + if (value instanceof PendingIntent) { + send(reason + " with key '" + key + "'", (PendingIntent) value); + } + } + } + } + + } +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java new file mode 100644 index 0000000000..6546e26ba7 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java @@ -0,0 +1,107 @@ +/* + * 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.cts.net.hostside; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.ConditionVariable; +import android.os.IBinder; +import android.os.RemoteException; + +import com.android.cts.net.hostside.IMyService; + +public class MyServiceClient { + private static final int TIMEOUT_MS = 5000; + private static final String PACKAGE = MyServiceClient.class.getPackage().getName(); + private static final String APP2_PACKAGE = PACKAGE + ".app2"; + private static final String SERVICE_NAME = APP2_PACKAGE + ".MyService"; + + private Context mContext; + private ServiceConnection mServiceConnection; + private IMyService mService; + + public MyServiceClient(Context context) { + mContext = context; + } + + public void bind() { + if (mService != null) { + throw new IllegalStateException("Already bound"); + } + + final ConditionVariable cv = new ConditionVariable(); + mServiceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + mService = IMyService.Stub.asInterface(service); + cv.open(); + } + @Override + public void onServiceDisconnected(ComponentName name) { + mService = null; + } + }; + + final Intent intent = new Intent(); + intent.setComponent(new ComponentName(APP2_PACKAGE, SERVICE_NAME)); + // Needs to use BIND_NOT_FOREGROUND so app2 does not run in + // the same process state as app + mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE + | Context.BIND_NOT_FOREGROUND); + cv.block(TIMEOUT_MS); + if (mService == null) { + throw new IllegalStateException( + "Could not bind to MyService service after " + TIMEOUT_MS + "ms"); + } + } + + public void unbind() { + if (mService != null) { + mContext.unbindService(mServiceConnection); + } + } + + public void registerBroadcastReceiver() throws RemoteException { + mService.registerBroadcastReceiver(); + } + + public int getCounters(String receiverName, String action) throws RemoteException { + return mService.getCounters(receiverName, action); + } + + public String checkNetworkStatus() throws RemoteException { + return mService.checkNetworkStatus(); + } + + public String getRestrictBackgroundStatus() throws RemoteException { + return mService.getRestrictBackgroundStatus(); + } + + public void sendNotification(int notificationId, String notificationType) throws RemoteException { + mService.sendNotification(notificationId, notificationType); + } + + public void registerNetworkCallback(INetworkCallback cb) throws RemoteException { + mService.registerNetworkCallback(cb); + } + + public void unregisterNetworkCallback() throws RemoteException { + mService.unregisterNetworkCallback(); + } +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java new file mode 100644 index 0000000000..7d3d4fce74 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java @@ -0,0 +1,184 @@ +/* + * 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 com.android.cts.net.hostside; + +import android.content.Intent; +import android.net.Network; +import android.net.ProxyInfo; +import android.net.VpnService; +import android.os.ParcelFileDescriptor; +import android.content.pm.PackageManager.NameNotFoundException; +import android.text.TextUtils; +import android.util.Log; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; + +public class MyVpnService extends VpnService { + + private static String TAG = "MyVpnService"; + private static int MTU = 1799; + + public static final String ACTION_ESTABLISHED = "com.android.cts.net.hostside.ESTABNLISHED"; + public static final String EXTRA_ALWAYS_ON = "is-always-on"; + public static final String EXTRA_LOCKDOWN_ENABLED = "is-lockdown-enabled"; + + private ParcelFileDescriptor mFd = null; + private PacketReflector mPacketReflector = null; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + String packageName = getPackageName(); + String cmd = intent.getStringExtra(packageName + ".cmd"); + if ("disconnect".equals(cmd)) { + stop(); + } else if ("connect".equals(cmd)) { + start(packageName, intent); + } + + return START_NOT_STICKY; + } + + private void start(String packageName, Intent intent) { + Builder builder = new Builder(); + + String addresses = intent.getStringExtra(packageName + ".addresses"); + if (addresses != null) { + String[] addressArray = addresses.split(","); + for (int i = 0; i < addressArray.length; i++) { + String[] prefixAndMask = addressArray[i].split("/"); + try { + InetAddress address = InetAddress.getByName(prefixAndMask[0]); + int prefixLength = Integer.parseInt(prefixAndMask[1]); + builder.addAddress(address, prefixLength); + } catch (UnknownHostException|NumberFormatException| + ArrayIndexOutOfBoundsException e) { + continue; + } + } + } + + String routes = intent.getStringExtra(packageName + ".routes"); + if (routes != null) { + String[] routeArray = routes.split(","); + for (int i = 0; i < routeArray.length; i++) { + String[] prefixAndMask = routeArray[i].split("/"); + try { + InetAddress address = InetAddress.getByName(prefixAndMask[0]); + int prefixLength = Integer.parseInt(prefixAndMask[1]); + builder.addRoute(address, prefixLength); + } catch (UnknownHostException|NumberFormatException| + ArrayIndexOutOfBoundsException e) { + continue; + } + } + } + + String allowed = intent.getStringExtra(packageName + ".allowedapplications"); + if (allowed != null) { + String[] packageArray = allowed.split(","); + for (int i = 0; i < packageArray.length; i++) { + String allowedPackage = packageArray[i]; + if (!TextUtils.isEmpty(allowedPackage)) { + try { + builder.addAllowedApplication(allowedPackage); + } catch(NameNotFoundException e) { + continue; + } + } + } + } + + String disallowed = intent.getStringExtra(packageName + ".disallowedapplications"); + if (disallowed != null) { + String[] packageArray = disallowed.split(","); + for (int i = 0; i < packageArray.length; i++) { + String disallowedPackage = packageArray[i]; + if (!TextUtils.isEmpty(disallowedPackage)) { + try { + builder.addDisallowedApplication(disallowedPackage); + } catch(NameNotFoundException e) { + continue; + } + } + } + } + + ArrayList underlyingNetworks = + intent.getParcelableArrayListExtra(packageName + ".underlyingNetworks"); + if (underlyingNetworks == null) { + // VPN tracks default network + builder.setUnderlyingNetworks(null); + } else { + builder.setUnderlyingNetworks(underlyingNetworks.toArray(new Network[0])); + } + + boolean isAlwaysMetered = intent.getBooleanExtra(packageName + ".isAlwaysMetered", false); + builder.setMetered(isAlwaysMetered); + + ProxyInfo vpnProxy = intent.getParcelableExtra(packageName + ".httpProxy"); + builder.setHttpProxy(vpnProxy); + builder.setMtu(MTU); + builder.setBlocking(true); + builder.setSession("MyVpnService"); + + Log.i(TAG, "Establishing VPN," + + " addresses=" + addresses + + " routes=" + routes + + " allowedApplications=" + allowed + + " disallowedApplications=" + disallowed); + + mFd = builder.establish(); + Log.i(TAG, "Established, fd=" + (mFd == null ? "null" : mFd.getFd())); + + broadcastEstablished(); + + mPacketReflector = new PacketReflector(mFd.getFileDescriptor(), MTU); + mPacketReflector.start(); + } + + private void broadcastEstablished() { + final Intent bcIntent = new Intent(ACTION_ESTABLISHED); + bcIntent.putExtra(EXTRA_ALWAYS_ON, isAlwaysOn()); + bcIntent.putExtra(EXTRA_LOCKDOWN_ENABLED, isLockdownEnabled()); + sendBroadcast(bcIntent); + } + + private void stop() { + if (mPacketReflector != null) { + mPacketReflector.interrupt(); + mPacketReflector = null; + } + try { + if (mFd != null) { + Log.i(TAG, "Closing filedescriptor"); + mFd.close(); + } + } catch(IOException e) { + } finally { + mFd = null; + } + } + + @Override + public void onDestroy() { + stop(); + super.onDestroy(); + } +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java new file mode 100644 index 0000000000..2ac29e77ff --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java @@ -0,0 +1,300 @@ +/* + * 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.cts.net.hostside; + +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isActiveNetworkMetered; +import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE; +import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE; + +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.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; + +import android.net.Network; +import android.net.NetworkCapabilities; +import android.util.Log; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.util.Objects; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +public class NetworkCallbackTest extends AbstractRestrictBackgroundNetworkTestCase { + private Network mNetwork; + private final TestNetworkCallback mTestNetworkCallback = new TestNetworkCallback(); + @Rule + public final MeterednessConfigurationRule mMeterednessConfiguration + = new MeterednessConfigurationRule(); + + enum CallbackState { + NONE, + AVAILABLE, + LOST, + BLOCKED_STATUS, + CAPABILITIES + } + + private static class CallbackInfo { + public final CallbackState state; + public final Network network; + public final Object arg; + + CallbackInfo(CallbackState s, Network n, Object o) { + state = s; network = n; arg = o; + } + + public String toString() { + return String.format("%s (%s) (%s)", state, network, arg); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof CallbackInfo)) return false; + // Ignore timeMs, since it's unpredictable. + final CallbackInfo other = (CallbackInfo) o; + return (state == other.state) && Objects.equals(network, other.network) + && Objects.equals(arg, other.arg); + } + + @Override + public int hashCode() { + return Objects.hash(state, network, arg); + } + } + + private class TestNetworkCallback extends INetworkCallback.Stub { + private static final int TEST_CONNECT_TIMEOUT_MS = 30_000; + private static final int TEST_CALLBACK_TIMEOUT_MS = 5_000; + + private final LinkedBlockingQueue mCallbacks = new LinkedBlockingQueue<>(); + + protected void setLastCallback(CallbackState state, Network network, Object o) { + mCallbacks.offer(new CallbackInfo(state, network, o)); + } + + CallbackInfo nextCallback(int timeoutMs) { + CallbackInfo cb = null; + try { + cb = mCallbacks.poll(timeoutMs, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + } + if (cb == null) { + fail("Did not receive callback after " + timeoutMs + "ms"); + } + return cb; + } + + CallbackInfo expectCallback(CallbackState state, Network expectedNetwork, Object o) { + final CallbackInfo expected = new CallbackInfo(state, expectedNetwork, o); + final CallbackInfo actual = nextCallback(TEST_CALLBACK_TIMEOUT_MS); + assertEquals("Unexpected callback:", expected, actual); + return actual; + } + + @Override + public void onAvailable(Network network) { + setLastCallback(CallbackState.AVAILABLE, network, null); + } + + @Override + public void onLost(Network network) { + setLastCallback(CallbackState.LOST, network, null); + } + + @Override + public void onBlockedStatusChanged(Network network, boolean blocked) { + setLastCallback(CallbackState.BLOCKED_STATUS, network, blocked); + } + + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities cap) { + setLastCallback(CallbackState.CAPABILITIES, network, cap); + } + + public Network expectAvailableCallbackAndGetNetwork() { + final CallbackInfo cb = nextCallback(TEST_CONNECT_TIMEOUT_MS); + if (cb.state != CallbackState.AVAILABLE) { + fail("Network is not available. Instead obtained the following callback :" + + cb); + } + return cb.network; + } + + public void expectBlockedStatusCallback(Network expectedNetwork, boolean expectBlocked) { + expectCallback(CallbackState.BLOCKED_STATUS, expectedNetwork, expectBlocked); + } + + public void expectBlockedStatusCallbackEventually(Network expectedNetwork, + boolean expectBlocked) { + final long deadline = System.currentTimeMillis() + TEST_CALLBACK_TIMEOUT_MS; + do { + final CallbackInfo cb = nextCallback((int) (deadline - System.currentTimeMillis())); + if (cb.state == CallbackState.BLOCKED_STATUS + && cb.network.equals(expectedNetwork)) { + assertEquals(expectBlocked, cb.arg); + return; + } + } while (System.currentTimeMillis() <= deadline); + fail("Didn't receive onBlockedStatusChanged()"); + } + + public void expectCapabilitiesCallbackEventually(Network expectedNetwork, boolean hasCap, + int cap) { + final long deadline = System.currentTimeMillis() + TEST_CALLBACK_TIMEOUT_MS; + do { + final CallbackInfo cb = nextCallback((int) (deadline - System.currentTimeMillis())); + if (cb.state != CallbackState.CAPABILITIES + || !expectedNetwork.equals(cb.network) + || (hasCap != ((NetworkCapabilities) cb.arg).hasCapability(cap))) { + Log.i("NetworkCallbackTest#expectCapabilitiesCallback", + "Ignoring non-matching callback : " + cb); + continue; + } + // Found a match, return + return; + } while (System.currentTimeMillis() <= deadline); + fail("Didn't receive the expected callback to onCapabilitiesChanged(). Check the " + + "log for a list of received callbacks, if any."); + } + } + + @Before + public void setUp() throws Exception { + super.setUp(); + + assumeTrue(isActiveNetworkMetered(true) || canChangeActiveNetworkMeteredness()); + + registerBroadcastReceiver(); + + removeRestrictBackgroundWhitelist(mUid); + removeRestrictBackgroundBlacklist(mUid); + assertRestrictBackgroundChangedReceived(0); + + // Initial state + setBatterySaverMode(false); + setRestrictBackground(false); + + // Make wifi a metered network. + mMeterednessConfiguration.configureNetworkMeteredness(true); + + // Register callback + registerNetworkCallback((INetworkCallback.Stub) mTestNetworkCallback); + // Once the wifi is marked as metered, the wifi will reconnect. Wait for onAvailable() + // callback to ensure wifi is connected before the test and store the default network. + mNetwork = mTestNetworkCallback.expectAvailableCallbackAndGetNetwork(); + // Check that the network is metered. + mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork, + false /* hasCapability */, NET_CAPABILITY_NOT_METERED); + mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false); + } + + @After + public void tearDown() throws Exception { + super.tearDown(); + + setRestrictBackground(false); + setBatterySaverMode(false); + unregisterNetworkCallback(); + } + + @RequiredProperties({DATA_SAVER_MODE}) + @Test + public void testOnBlockedStatusChanged_dataSaver() throws Exception { + try { + // Enable restrict background + setRestrictBackground(true); + assertBackgroundNetworkAccess(false); + mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true); + + // Add to whitelist + addRestrictBackgroundWhitelist(mUid); + assertBackgroundNetworkAccess(true); + mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false); + + // Remove from whitelist + removeRestrictBackgroundWhitelist(mUid); + assertBackgroundNetworkAccess(false); + mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true); + } finally { + mMeterednessConfiguration.resetNetworkMeteredness(); + } + + // Set to non-metered network + mMeterednessConfiguration.configureNetworkMeteredness(false); + mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork, + true /* hasCapability */, NET_CAPABILITY_NOT_METERED); + try { + assertBackgroundNetworkAccess(true); + mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false); + + // Disable restrict background, should not trigger callback + setRestrictBackground(false); + assertBackgroundNetworkAccess(true); + } finally { + mMeterednessConfiguration.resetNetworkMeteredness(); + } + } + + @RequiredProperties({BATTERY_SAVER_MODE}) + @Test + public void testOnBlockedStatusChanged_powerSaver() throws Exception { + try { + // Enable Power Saver + setBatterySaverMode(true); + assertBackgroundNetworkAccess(false); + mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true); + + // Disable Power Saver + setBatterySaverMode(false); + assertBackgroundNetworkAccess(true); + mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false); + } finally { + mMeterednessConfiguration.resetNetworkMeteredness(); + } + + // Set to non-metered network + mMeterednessConfiguration.configureNetworkMeteredness(false); + mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork, + true /* hasCapability */, NET_CAPABILITY_NOT_METERED); + try { + // Enable Power Saver + setBatterySaverMode(true); + assertBackgroundNetworkAccess(false); + mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true); + + // Disable Power Saver + setBatterySaverMode(false); + assertBackgroundNetworkAccess(true); + mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false); + } finally { + mMeterednessConfiguration.resetNetworkMeteredness(); + } + } + + // TODO: 1. test against VPN lockdown. + // 2. test against multiple networks. +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestRunner.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestRunner.java new file mode 100644 index 0000000000..f340907ae5 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestRunner.java @@ -0,0 +1,44 @@ +/* + * 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.cts.net.hostside; + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner; + +import org.junit.rules.RunRules; +import org.junit.rules.TestRule; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.InitializationError; +import org.junit.runners.model.Statement; + +import java.util.List; + +/** + * Custom runner to allow dumping logs after a test failure before the @After methods get to run. + */ +public class NetworkPolicyTestRunner extends AndroidJUnit4ClassRunner { + private TestRule mDumpOnFailureRule = new DumpOnFailureRule(); + + public NetworkPolicyTestRunner(Class klass) throws InitializationError { + super(klass); + } + + @Override + public Statement methodInvoker(FrameworkMethod method, Object test) { + return new RunRules(super.methodInvoker(method, test), List.of(mDumpOnFailureRule), + describeChild(method)); + } +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java new file mode 100644 index 0000000000..3807d79c35 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java @@ -0,0 +1,284 @@ +/* + * 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.cts.net.hostside; + +import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED; +import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED; +import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; + +import static com.android.compatibility.common.util.SystemUtil.runShellCommand; +import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG; + +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.app.ActivityManager; +import android.app.Instrumentation; +import android.content.Context; +import android.location.LocationManager; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.wifi.WifiManager; +import android.os.Process; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; + +import com.android.compatibility.common.util.AppStandbyUtils; +import com.android.compatibility.common.util.BatteryUtils; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import androidx.test.platform.app.InstrumentationRegistry; + +public class NetworkPolicyTestUtils { + + private static final int TIMEOUT_CHANGE_METEREDNESS_MS = 5000; + + private static ConnectivityManager mCm; + private static WifiManager mWm; + + private static Boolean mBatterySaverSupported; + private static Boolean mDataSaverSupported; + private static Boolean mDozeModeSupported; + private static Boolean mAppStandbySupported; + + private NetworkPolicyTestUtils() {} + + public static boolean isBatterySaverSupported() { + if (mBatterySaverSupported == null) { + mBatterySaverSupported = BatteryUtils.isBatterySaverSupported(); + } + return mBatterySaverSupported; + } + + /** + * As per CDD requirements, if the device doesn't support data saver mode then + * ConnectivityManager.getRestrictBackgroundStatus() will always return + * RESTRICT_BACKGROUND_STATUS_DISABLED. So, enable the data saver mode and check if + * ConnectivityManager.getRestrictBackgroundStatus() for an app in background returns + * RESTRICT_BACKGROUND_STATUS_DISABLED or not. + */ + public static boolean isDataSaverSupported() { + if (mDataSaverSupported == null) { + assertMyRestrictBackgroundStatus(RESTRICT_BACKGROUND_STATUS_DISABLED); + try { + setRestrictBackground(true); + mDataSaverSupported = !isMyRestrictBackgroundStatus( + RESTRICT_BACKGROUND_STATUS_DISABLED); + } finally { + setRestrictBackground(false); + } + } + return mDataSaverSupported; + } + + public static boolean isDozeModeSupported() { + if (mDozeModeSupported == null) { + final String result = executeShellCommand("cmd deviceidle enabled deep"); + mDozeModeSupported = result.equals("1"); + } + return mDozeModeSupported; + } + + public static boolean isAppStandbySupported() { + if (mAppStandbySupported == null) { + mAppStandbySupported = AppStandbyUtils.isAppStandbyEnabled(); + } + return mAppStandbySupported; + } + + public static boolean isLowRamDevice() { + final ActivityManager am = (ActivityManager) getContext().getSystemService( + Context.ACTIVITY_SERVICE); + return am.isLowRamDevice(); + } + + public static boolean isLocationEnabled() { + final LocationManager lm = (LocationManager) getContext().getSystemService( + Context.LOCATION_SERVICE); + return lm.isLocationEnabled(); + } + + public static void setLocationEnabled(boolean enabled) { + final LocationManager lm = (LocationManager) getContext().getSystemService( + Context.LOCATION_SERVICE); + lm.setLocationEnabledForUser(enabled, Process.myUserHandle()); + assertEquals("Couldn't change location enabled state", lm.isLocationEnabled(), enabled); + Log.d(TAG, "Changed location enabled state to " + enabled); + } + + public static boolean isActiveNetworkMetered(boolean metered) { + return getConnectivityManager().isActiveNetworkMetered() == metered; + } + + public static boolean canChangeActiveNetworkMeteredness() { + final Network activeNetwork = getConnectivityManager().getActiveNetwork(); + final NetworkCapabilities networkCapabilities + = getConnectivityManager().getNetworkCapabilities(activeNetwork); + return networkCapabilities.hasTransport(TRANSPORT_WIFI); + } + + public static Pair setupMeteredNetwork(boolean metered) throws Exception { + if (isActiveNetworkMetered(metered)) { + return null; + } + final boolean isLocationEnabled = isLocationEnabled(); + try { + if (!isLocationEnabled) { + setLocationEnabled(true); + } + final String ssid = unquoteSSID(getWifiManager().getConnectionInfo().getSSID()); + assertNotEquals(WifiManager.UNKNOWN_SSID, ssid); + setWifiMeteredStatus(ssid, metered); + return Pair.create(ssid, !metered); + } finally { + // Reset the location enabled state + if (!isLocationEnabled) { + setLocationEnabled(false); + } + } + } + + public static void resetMeteredNetwork(String ssid, boolean metered) throws Exception { + setWifiMeteredStatus(ssid, metered); + } + + public static void setWifiMeteredStatus(String ssid, boolean metered) throws Exception { + assertFalse("SSID should not be empty", TextUtils.isEmpty(ssid)); + final String cmd = "cmd netpolicy set metered-network " + ssid + " " + metered; + executeShellCommand(cmd); + assertWifiMeteredStatus(ssid, metered); + assertActiveNetworkMetered(metered); + } + + public static void assertWifiMeteredStatus(String ssid, boolean expectedMeteredStatus) { + final String result = executeShellCommand("cmd netpolicy list wifi-networks"); + final String expectedLine = ssid + ";" + expectedMeteredStatus; + assertTrue("Expected line: " + expectedLine + "; Actual result: " + result, + result.contains(expectedLine)); + } + + // Copied from cts/tests/tests/net/src/android/net/cts/ConnectivityManagerTest.java + public static void assertActiveNetworkMetered(boolean expectedMeteredStatus) throws Exception { + final CountDownLatch latch = new CountDownLatch(1); + final NetworkCallback networkCallback = new NetworkCallback() { + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) { + final boolean metered = !nc.hasCapability(NET_CAPABILITY_NOT_METERED); + if (metered == expectedMeteredStatus) { + latch.countDown(); + } + } + }; + // Registering a callback here guarantees onCapabilitiesChanged is called immediately + // with the current setting. Therefore, if the setting has already been changed, + // this method will return right away, and if not it will wait for the setting to change. + getConnectivityManager().registerDefaultNetworkCallback(networkCallback); + if (!latch.await(TIMEOUT_CHANGE_METEREDNESS_MS, TimeUnit.MILLISECONDS)) { + fail("Timed out waiting for active network metered status to change to " + + expectedMeteredStatus + " ; network = " + + getConnectivityManager().getActiveNetwork()); + } + getConnectivityManager().unregisterNetworkCallback(networkCallback); + } + + public static void setRestrictBackground(boolean enabled) { + executeShellCommand("cmd netpolicy set restrict-background " + enabled); + final String output = executeShellCommand("cmd netpolicy get restrict-background"); + final String expectedSuffix = enabled ? "enabled" : "disabled"; + assertTrue("output '" + output + "' should end with '" + expectedSuffix + "'", + output.endsWith(expectedSuffix)); + } + + public static boolean isMyRestrictBackgroundStatus(int expectedStatus) { + final int actualStatus = getConnectivityManager().getRestrictBackgroundStatus(); + if (expectedStatus != actualStatus) { + Log.d(TAG, "MyRestrictBackgroundStatus: " + + "Expected: " + restrictBackgroundValueToString(expectedStatus) + + "; Actual: " + restrictBackgroundValueToString(actualStatus)); + return false; + } + return true; + } + + // Copied from cts/tests/tests/net/src/android/net/cts/ConnectivityManagerTest.java + private static String unquoteSSID(String ssid) { + // SSID is returned surrounded by quotes if it can be decoded as UTF-8. + // Otherwise it's guaranteed not to start with a quote. + if (ssid.charAt(0) == '"') { + return ssid.substring(1, ssid.length() - 1); + } else { + return ssid; + } + } + + public static String restrictBackgroundValueToString(int status) { + switch (status) { + case RESTRICT_BACKGROUND_STATUS_DISABLED: + return "DISABLED"; + case RESTRICT_BACKGROUND_STATUS_WHITELISTED: + return "WHITELISTED"; + case RESTRICT_BACKGROUND_STATUS_ENABLED: + return "ENABLED"; + default: + return "UNKNOWN_STATUS_" + status; + } + } + + public static String executeShellCommand(String command) { + final String result = runShellCommand(command).trim(); + Log.d(TAG, "Output of '" + command + "': '" + result + "'"); + return result; + } + + public static void assertMyRestrictBackgroundStatus(int expectedStatus) { + final int actualStatus = getConnectivityManager().getRestrictBackgroundStatus(); + assertEquals(restrictBackgroundValueToString(expectedStatus), + restrictBackgroundValueToString(actualStatus)); + } + + public static ConnectivityManager getConnectivityManager() { + if (mCm == null) { + mCm = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE); + } + return mCm; + } + + public static WifiManager getWifiManager() { + if (mWm == null) { + mWm = (WifiManager) getContext().getSystemService(Context.WIFI_SERVICE); + } + return mWm; + } + + public static Context getContext() { + return getInstrumentation().getContext(); + } + + public static Instrumentation getInstrumentation() { + return InstrumentationRegistry.getInstrumentation(); + } +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/PacketReflector.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/PacketReflector.java new file mode 100644 index 0000000000..124c2c3862 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/PacketReflector.java @@ -0,0 +1,254 @@ +/* + * 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 com.android.cts.net.hostside; + +import static android.system.OsConstants.ICMP6_ECHO_REPLY; +import static android.system.OsConstants.ICMP6_ECHO_REQUEST; +import static android.system.OsConstants.ICMP_ECHO; +import static android.system.OsConstants.ICMP_ECHOREPLY; + +import android.system.ErrnoException; +import android.system.Os; +import android.util.Log; + +import java.io.FileDescriptor; +import java.io.IOException; + +public class PacketReflector extends Thread { + + private static int IPV4_HEADER_LENGTH = 20; + private static int IPV6_HEADER_LENGTH = 40; + + private static int IPV4_ADDR_OFFSET = 12; + private static int IPV6_ADDR_OFFSET = 8; + private static int IPV4_ADDR_LENGTH = 4; + private static int IPV6_ADDR_LENGTH = 16; + + private static int IPV4_PROTO_OFFSET = 9; + private static int IPV6_PROTO_OFFSET = 6; + + private static final byte IPPROTO_ICMP = 1; + private static final byte IPPROTO_TCP = 6; + private static final byte IPPROTO_UDP = 17; + private static final byte IPPROTO_ICMPV6 = 58; + + private static int ICMP_HEADER_LENGTH = 8; + private static int TCP_HEADER_LENGTH = 20; + private static int UDP_HEADER_LENGTH = 8; + + private static final byte ICMP_ECHO = 8; + private static final byte ICMP_ECHOREPLY = 0; + + private static String TAG = "PacketReflector"; + + private FileDescriptor mFd; + private byte[] mBuf; + + public PacketReflector(FileDescriptor fd, int mtu) { + super("PacketReflector"); + mFd = fd; + mBuf = new byte[mtu]; + } + + private static void swapBytes(byte[] buf, int pos1, int pos2, int len) { + for (int i = 0; i < len; i++) { + byte b = buf[pos1 + i]; + buf[pos1 + i] = buf[pos2 + i]; + buf[pos2 + i] = b; + } + } + + private static void swapAddresses(byte[] buf, int version) { + int addrPos, addrLen; + switch(version) { + case 4: + addrPos = IPV4_ADDR_OFFSET; + addrLen = IPV4_ADDR_LENGTH; + break; + case 6: + addrPos = IPV6_ADDR_OFFSET; + addrLen = IPV6_ADDR_LENGTH; + break; + default: + throw new IllegalArgumentException(); + } + swapBytes(buf, addrPos, addrPos + addrLen, addrLen); + } + + // Reflect TCP packets: swap the source and destination addresses, but don't change the ports. + // This is used by the test to "connect to itself" through the VPN. + private void processTcpPacket(byte[] buf, int version, int len, int hdrLen) { + if (len < hdrLen + TCP_HEADER_LENGTH) { + return; + } + + // Swap src and dst IP addresses. + swapAddresses(buf, version); + + // Send the packet back. + writePacket(buf, len); + } + + // Echo UDP packets: swap source and destination addresses, and source and destination ports. + // This is used by the test to check that the bytes it sends are echoed back. + private void processUdpPacket(byte[] buf, int version, int len, int hdrLen) { + if (len < hdrLen + UDP_HEADER_LENGTH) { + return; + } + + // Swap src and dst IP addresses. + swapAddresses(buf, version); + + // Swap dst and src ports. + int portOffset = hdrLen; + swapBytes(buf, portOffset, portOffset + 2, 2); + + // Send the packet back. + writePacket(buf, len); + } + + private void processIcmpPacket(byte[] buf, int version, int len, int hdrLen) { + if (len < hdrLen + ICMP_HEADER_LENGTH) { + return; + } + + byte type = buf[hdrLen]; + if (!(version == 4 && type == ICMP_ECHO) && + !(version == 6 && type == (byte) ICMP6_ECHO_REQUEST)) { + return; + } + + // Save the ping packet we received. + byte[] request = buf.clone(); + + // Swap src and dst IP addresses, and send the packet back. + // This effectively pings the device to see if it replies. + swapAddresses(buf, version); + writePacket(buf, len); + + // The device should have replied, and buf should now contain a ping response. + int received = readPacket(buf); + if (received != len) { + Log.i(TAG, "Reflecting ping did not result in ping response: " + + "read=" + received + " expected=" + len); + return; + } + + byte replyType = buf[hdrLen]; + if ((type == ICMP_ECHO && replyType != ICMP_ECHOREPLY) + || (type == (byte) ICMP6_ECHO_REQUEST && replyType != (byte) ICMP6_ECHO_REPLY)) { + Log.i(TAG, "Received unexpected ICMP reply: original " + type + + ", reply " + replyType); + return; + } + + // Compare the response we got with the original packet. + // The only thing that should have changed are addresses, type and checksum. + // Overwrite them with the received bytes and see if the packet is otherwise identical. + request[hdrLen] = buf[hdrLen]; // Type + request[hdrLen + 2] = buf[hdrLen + 2]; // Checksum byte 1. + request[hdrLen + 3] = buf[hdrLen + 3]; // Checksum byte 2. + + // Since Linux kernel 4.2, net.ipv6.auto_flowlabels is set by default, and therefore + // the request and reply may have different IPv6 flow label: ignore that as well. + if (version == 6) { + request[1] = (byte)(request[1] & 0xf0 | buf[1] & 0x0f); + request[2] = buf[2]; + request[3] = buf[3]; + } + + for (int i = 0; i < len; i++) { + if (buf[i] != request[i]) { + Log.i(TAG, "Received non-matching packet when expecting ping response."); + return; + } + } + + // Now swap the addresses again and reflect the packet. This sends a ping reply. + swapAddresses(buf, version); + writePacket(buf, len); + } + + private void writePacket(byte[] buf, int len) { + try { + Os.write(mFd, buf, 0, len); + } catch (ErrnoException|IOException e) { + Log.e(TAG, "Error writing packet: " + e.getMessage()); + } + } + + private int readPacket(byte[] buf) { + int len; + try { + len = Os.read(mFd, buf, 0, buf.length); + } catch (ErrnoException|IOException e) { + Log.e(TAG, "Error reading packet: " + e.getMessage()); + len = -1; + } + return len; + } + + // Reads one packet from our mFd, and possibly writes the packet back. + private void processPacket() { + int len = readPacket(mBuf); + if (len < 1) { + return; + } + + int version = mBuf[0] >> 4; + int addrPos, protoPos, hdrLen, addrLen; + if (version == 4) { + hdrLen = IPV4_HEADER_LENGTH; + protoPos = IPV4_PROTO_OFFSET; + addrPos = IPV4_ADDR_OFFSET; + addrLen = IPV4_ADDR_LENGTH; + } else if (version == 6) { + hdrLen = IPV6_HEADER_LENGTH; + protoPos = IPV6_PROTO_OFFSET; + addrPos = IPV6_ADDR_OFFSET; + addrLen = IPV6_ADDR_LENGTH; + } else { + return; + } + + if (len < hdrLen) { + return; + } + + byte proto = mBuf[protoPos]; + switch (proto) { + case IPPROTO_ICMP: + case IPPROTO_ICMPV6: + processIcmpPacket(mBuf, version, len, hdrLen); + break; + case IPPROTO_TCP: + processTcpPacket(mBuf, version, len, hdrLen); + break; + case IPPROTO_UDP: + processUdpPacket(mBuf, version, len, hdrLen); + break; + } + } + + public void run() { + Log.i(TAG, "PacketReflector starting fd=" + mFd + " valid=" + mFd.valid()); + while (!interrupted() && mFd.valid()) { + processPacket(); + } + Log.i(TAG, "PacketReflector exiting fd=" + mFd + " valid=" + mFd.valid()); + } +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/Property.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/Property.java new file mode 100644 index 0000000000..18805f9613 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/Property.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.cts.net.hostside; + +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isActiveNetworkMetered; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isAppStandbySupported; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isBatterySaverSupported; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isDataSaverSupported; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isDozeModeSupported; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isLowRamDevice; + +public enum Property { + BATTERY_SAVER_MODE(1 << 0) { + public boolean isSupported() { return isBatterySaverSupported(); } + }, + + DATA_SAVER_MODE(1 << 1) { + public boolean isSupported() { return isDataSaverSupported(); } + }, + + NO_DATA_SAVER_MODE(~DATA_SAVER_MODE.getValue()) { + public boolean isSupported() { return !isDataSaverSupported(); } + }, + + DOZE_MODE(1 << 2) { + public boolean isSupported() { return isDozeModeSupported(); } + }, + + APP_STANDBY_MODE(1 << 3) { + public boolean isSupported() { return isAppStandbySupported(); } + }, + + NOT_LOW_RAM_DEVICE(1 << 4) { + public boolean isSupported() { return !isLowRamDevice(); } + }, + + METERED_NETWORK(1 << 5) { + public boolean isSupported() { + return isActiveNetworkMetered(true) || canChangeActiveNetworkMeteredness(); + } + }, + + NON_METERED_NETWORK(~METERED_NETWORK.getValue()) { + public boolean isSupported() { + return isActiveNetworkMetered(false) || canChangeActiveNetworkMeteredness(); + } + }; + + private int mValue; + + Property(int value) { mValue = value; } + + public int getValue() { return mValue; } + + abstract boolean isSupported(); +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RemoteSocketFactoryClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RemoteSocketFactoryClient.java new file mode 100644 index 0000000000..80f99b6605 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RemoteSocketFactoryClient.java @@ -0,0 +1,100 @@ +/* + * 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.cts.net.hostside; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.ConditionVariable; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.system.ErrnoException; +import android.system.Os; + +import com.android.cts.net.hostside.IRemoteSocketFactory; + +import java.io.FileDescriptor; +import java.io.IOException; + +public class RemoteSocketFactoryClient { + private static final int TIMEOUT_MS = 5000; + private static final String PACKAGE = RemoteSocketFactoryClient.class.getPackage().getName(); + private static final String APP2_PACKAGE = PACKAGE + ".app2"; + private static final String SERVICE_NAME = APP2_PACKAGE + ".RemoteSocketFactoryService"; + + private Context mContext; + private ServiceConnection mServiceConnection; + private IRemoteSocketFactory mService; + + public RemoteSocketFactoryClient(Context context) { + mContext = context; + } + + public void bind() { + if (mService != null) { + throw new IllegalStateException("Already bound"); + } + + final ConditionVariable cv = new ConditionVariable(); + mServiceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + mService = IRemoteSocketFactory.Stub.asInterface(service); + cv.open(); + } + @Override + public void onServiceDisconnected(ComponentName name) { + mService = null; + } + }; + + final Intent intent = new Intent(); + intent.setComponent(new ComponentName(APP2_PACKAGE, SERVICE_NAME)); + mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); + cv.block(TIMEOUT_MS); + if (mService == null) { + throw new IllegalStateException( + "Could not bind to RemoteSocketFactory service after " + TIMEOUT_MS + "ms"); + } + } + + public void unbind() { + if (mService != null) { + mContext.unbindService(mServiceConnection); + } + } + + public FileDescriptor openSocketFd(String host, int port, int timeoutMs) + throws RemoteException, ErrnoException, IOException { + // Dup the filedescriptor so ParcelFileDescriptor's finalizer doesn't garbage collect it + // and cause our fd to become invalid. http://b/35927643 . + ParcelFileDescriptor pfd = mService.openSocketFd(host, port, timeoutMs); + FileDescriptor fd = Os.dup(pfd.getFileDescriptor()); + pfd.close(); + return fd; + } + + public String getPackageName() throws RemoteException { + return mService.getPackageName(); + } + + public int getUid() throws RemoteException { + return mService.getUid(); + } +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredProperties.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredProperties.java new file mode 100644 index 0000000000..96838bba0a --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredProperties.java @@ -0,0 +1,31 @@ +/* + * 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.cts.net.hostside; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target({METHOD, TYPE}) +@Inherited +public @interface RequiredProperties { + Property[] value(); +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredPropertiesRule.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredPropertiesRule.java new file mode 100644 index 0000000000..01f9f3ea81 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredPropertiesRule.java @@ -0,0 +1,94 @@ +/* + * 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.cts.net.hostside; + +import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG; + +import android.text.TextUtils; +import android.util.ArraySet; +import android.util.Log; + +import com.android.compatibility.common.util.BeforeAfterRule; + +import org.junit.Assume; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.util.ArrayList; +import java.util.Collections; + +public class RequiredPropertiesRule extends BeforeAfterRule { + + private static ArraySet mRequiredProperties; + + @Override + public void onBefore(Statement base, Description description) { + mRequiredProperties = getAllRequiredProperties(description); + + final String testName = description.getClassName() + "#" + description.getMethodName(); + assertTestIsValid(testName, mRequiredProperties); + Log.i(TAG, "Running test " + testName + " with required properties: " + + propertiesToString(mRequiredProperties)); + } + + private ArraySet getAllRequiredProperties(Description description) { + final ArraySet allRequiredProperties = new ArraySet<>(); + RequiredProperties requiredProperties = description.getAnnotation(RequiredProperties.class); + if (requiredProperties != null) { + Collections.addAll(allRequiredProperties, requiredProperties.value()); + } + + for (Class clazz = description.getTestClass(); + clazz != null; clazz = clazz.getSuperclass()) { + requiredProperties = clazz.getDeclaredAnnotation(RequiredProperties.class); + if (requiredProperties == null) { + continue; + } + for (Property requiredProperty : requiredProperties.value()) { + for (Property p : Property.values()) { + if (p.getValue() == ~requiredProperty.getValue() + && allRequiredProperties.contains(p)) { + continue; + } + } + allRequiredProperties.add(requiredProperty); + } + } + return allRequiredProperties; + } + + private void assertTestIsValid(String testName, ArraySet requiredProperies) { + if (requiredProperies == null) { + return; + } + final ArrayList unsupportedProperties = new ArrayList<>(); + for (Property property : requiredProperies) { + if (!property.isSupported()) { + unsupportedProperties.add(property); + } + } + Assume.assumeTrue("Unsupported properties: " + + propertiesToString(unsupportedProperties), unsupportedProperties.isEmpty()); + } + + public static ArraySet getRequiredProperties() { + return mRequiredProperties; + } + + private static String propertiesToString(Iterable properties) { + return "[" + TextUtils.join(",", properties) + "]"; + } +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java new file mode 100755 index 0000000000..81a431cfda --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java @@ -0,0 +1,1165 @@ +/* + * 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 com.android.cts.net.hostside; + +import static android.os.Process.INVALID_UID; +import static android.system.OsConstants.AF_INET; +import static android.system.OsConstants.AF_INET6; +import static android.system.OsConstants.ECONNABORTED; +import static android.system.OsConstants.IPPROTO_ICMP; +import static android.system.OsConstants.IPPROTO_ICMPV6; +import static android.system.OsConstants.IPPROTO_TCP; +import static android.system.OsConstants.POLLIN; +import static android.system.OsConstants.SOCK_DGRAM; + +import android.annotation.Nullable; +import android.app.DownloadManager; +import android.app.DownloadManager.Query; +import android.app.DownloadManager.Request; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.Proxy; +import android.net.ProxyInfo; +import android.net.Uri; +import android.net.VpnService; +import android.net.wifi.WifiManager; +import android.os.ParcelFileDescriptor; +import android.os.Process; +import android.os.SystemProperties; +import android.provider.Settings; +import android.support.test.uiautomator.UiDevice; +import android.support.test.uiautomator.UiObject; +import android.support.test.uiautomator.UiSelector; +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; +import android.system.StructPollfd; +import android.test.InstrumentationTestCase; +import android.test.MoreAsserts; +import android.text.TextUtils; +import android.util.Log; + +import com.android.compatibility.common.util.BlockingBroadcastReceiver; + +import java.io.Closeable; +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Objects; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Tests for the VpnService API. + * + * These tests establish a VPN via the VpnService API, and have the service reflect the packets back + * to the device without causing any network traffic. This allows testing the local VPN data path + * without a network connection or a VPN server. + * + * Note: in Lollipop, VPN functionality relies on kernel support for UID-based routing. If these + * tests fail, it may be due to the lack of kernel support. The necessary patches can be + * cherry-picked from the Android common kernel trees: + * + * android-3.10: + * https://android-review.googlesource.com/#/c/99220/ + * https://android-review.googlesource.com/#/c/100545/ + * + * android-3.4: + * https://android-review.googlesource.com/#/c/99225/ + * https://android-review.googlesource.com/#/c/100557/ + * + * To ensure that the kernel has the required commits, run the kernel unit + * tests described at: + * + * https://source.android.com/devices/tech/config/kernel_network_tests.html + * + */ +public class VpnTest extends InstrumentationTestCase { + + // These are neither public nor @TestApi. + // TODO: add them to @TestApi. + private static final String PRIVATE_DNS_MODE_SETTING = "private_dns_mode"; + private static final String PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = "hostname"; + private static final String PRIVATE_DNS_MODE_OPPORTUNISTIC = "opportunistic"; + private static final String PRIVATE_DNS_SPECIFIER_SETTING = "private_dns_specifier"; + + public static String TAG = "VpnTest"; + public static int TIMEOUT_MS = 3 * 1000; + public static int SOCKET_TIMEOUT_MS = 100; + public static String TEST_HOST = "connectivitycheck.gstatic.com"; + + private UiDevice mDevice; + private MyActivity mActivity; + private String mPackageName; + private ConnectivityManager mCM; + private WifiManager mWifiManager; + private RemoteSocketFactoryClient mRemoteSocketFactoryClient; + + Network mNetwork; + NetworkCallback mCallback; + final Object mLock = new Object(); + final Object mLockShutdown = new Object(); + + private String mOldPrivateDnsMode; + private String mOldPrivateDnsSpecifier; + + private boolean supportedHardware() { + final PackageManager pm = getInstrumentation().getContext().getPackageManager(); + return !pm.hasSystemFeature("android.hardware.type.watch"); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + + mNetwork = null; + mCallback = null; + storePrivateDnsSetting(); + + mDevice = UiDevice.getInstance(getInstrumentation()); + mActivity = launchActivity(getInstrumentation().getTargetContext().getPackageName(), + MyActivity.class, null); + mPackageName = mActivity.getPackageName(); + mCM = (ConnectivityManager) mActivity.getSystemService(Context.CONNECTIVITY_SERVICE); + mWifiManager = (WifiManager) mActivity.getSystemService(Context.WIFI_SERVICE); + mRemoteSocketFactoryClient = new RemoteSocketFactoryClient(mActivity); + mRemoteSocketFactoryClient.bind(); + mDevice.waitForIdle(); + } + + @Override + public void tearDown() throws Exception { + restorePrivateDnsSetting(); + mRemoteSocketFactoryClient.unbind(); + if (mCallback != null) { + mCM.unregisterNetworkCallback(mCallback); + } + Log.i(TAG, "Stopping VPN"); + stopVpn(); + mActivity.finish(); + super.tearDown(); + } + + private void prepareVpn() throws Exception { + final int REQUEST_ID = 42; + + // Attempt to prepare. + Log.i(TAG, "Preparing VPN"); + Intent intent = VpnService.prepare(mActivity); + + if (intent != null) { + // Start the confirmation dialog and click OK. + mActivity.startActivityForResult(intent, REQUEST_ID); + mDevice.waitForIdle(); + + String packageName = intent.getComponent().getPackageName(); + String resourceIdRegex = "android:id/button1$|button_start_vpn"; + final UiObject okButton = new UiObject(new UiSelector() + .className("android.widget.Button") + .packageName(packageName) + .resourceIdMatches(resourceIdRegex)); + if (okButton.waitForExists(TIMEOUT_MS) == false) { + mActivity.finishActivity(REQUEST_ID); + fail("VpnService.prepare returned an Intent for '" + intent.getComponent() + "' " + + "to display the VPN confirmation dialog, but this test could not find the " + + "button to allow the VPN application to connect. Please ensure that the " + + "component displays a button with a resource ID matching the regexp: '" + + resourceIdRegex + "'."); + } + + // Click the button and wait for RESULT_OK. + okButton.click(); + try { + int result = mActivity.getResult(TIMEOUT_MS); + if (result != MyActivity.RESULT_OK) { + fail("The VPN confirmation dialog did not return RESULT_OK when clicking on " + + "the button matching the regular expression '" + resourceIdRegex + + "' of " + intent.getComponent() + "'. Please ensure that clicking on " + + "that button allows the VPN application to connect. " + + "Return value: " + result); + } + } catch (InterruptedException e) { + fail("VPN confirmation dialog did not return after " + TIMEOUT_MS + "ms"); + } + + // Now we should be prepared. + intent = VpnService.prepare(mActivity); + if (intent != null) { + fail("VpnService.prepare returned non-null even after the VPN dialog " + + intent.getComponent() + "returned RESULT_OK."); + } + } + } + + // TODO: Consider replacing arguments with a Builder. + private void startVpn( + String[] addresses, String[] routes, String allowedApplications, + String disallowedApplications, @Nullable ProxyInfo proxyInfo, + @Nullable ArrayList underlyingNetworks, boolean isAlwaysMetered) throws Exception { + prepareVpn(); + + // Register a callback so we will be notified when our VPN comes up. + final NetworkRequest request = new NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_VPN) + .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) + .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build(); + mCallback = new NetworkCallback() { + public void onAvailable(Network network) { + synchronized (mLock) { + Log.i(TAG, "Got available callback for network=" + network); + mNetwork = network; + mLock.notify(); + } + } + }; + mCM.registerNetworkCallback(request, mCallback); // Unregistered in tearDown. + + // Start the service and wait up for TIMEOUT_MS ms for the VPN to come up. + Intent intent = new Intent(mActivity, MyVpnService.class) + .putExtra(mPackageName + ".cmd", "connect") + .putExtra(mPackageName + ".addresses", TextUtils.join(",", addresses)) + .putExtra(mPackageName + ".routes", TextUtils.join(",", routes)) + .putExtra(mPackageName + ".allowedapplications", allowedApplications) + .putExtra(mPackageName + ".disallowedapplications", disallowedApplications) + .putExtra(mPackageName + ".httpProxy", proxyInfo) + .putParcelableArrayListExtra( + mPackageName + ".underlyingNetworks", underlyingNetworks) + .putExtra(mPackageName + ".isAlwaysMetered", isAlwaysMetered); + + mActivity.startService(intent); + synchronized (mLock) { + if (mNetwork == null) { + Log.i(TAG, "bf mLock"); + mLock.wait(TIMEOUT_MS); + Log.i(TAG, "af mLock"); + } + } + + if (mNetwork == null) { + fail("VPN did not become available after " + TIMEOUT_MS + "ms"); + } + + // Unfortunately, when the available callback fires, the VPN UID ranges are not yet + // configured. Give the system some time to do so. http://b/18436087 . + try { Thread.sleep(3000); } catch(InterruptedException e) {} + } + + private void stopVpn() { + // Register a callback so we will be notified when our VPN comes up. + final NetworkRequest request = new NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_VPN) + .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) + .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build(); + mCallback = new NetworkCallback() { + public void onLost(Network network) { + synchronized (mLockShutdown) { + Log.i(TAG, "Got lost callback for network=" + network + + ",mNetwork = " + mNetwork); + if( mNetwork == network){ + mLockShutdown.notify(); + } + } + } + }; + mCM.registerNetworkCallback(request, mCallback); // Unregistered in tearDown. + // Simply calling mActivity.stopService() won't stop the service, because the system binds + // to the service for the purpose of sending it a revoke command if another VPN comes up, + // and stopping a bound service has no effect. Instead, "start" the service again with an + // Intent that tells it to disconnect. + Intent intent = new Intent(mActivity, MyVpnService.class) + .putExtra(mPackageName + ".cmd", "disconnect"); + mActivity.startService(intent); + synchronized (mLockShutdown) { + try { + Log.i(TAG, "bf mLockShutdown"); + mLockShutdown.wait(TIMEOUT_MS); + Log.i(TAG, "af mLockShutdown"); + } catch(InterruptedException e) {} + } + } + + private static void closeQuietly(Closeable c) { + if (c != null) { + try { + c.close(); + } catch (IOException e) { + } + } + } + + private static void checkPing(String to) throws IOException, ErrnoException { + InetAddress address = InetAddress.getByName(to); + FileDescriptor s; + final int LENGTH = 64; + byte[] packet = new byte[LENGTH]; + byte[] header; + + // Construct a ping packet. + Random random = new Random(); + random.nextBytes(packet); + if (address instanceof Inet6Address) { + s = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6); + header = new byte[] { (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; + } else { + // Note that this doesn't actually work due to http://b/18558481 . + s = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP); + header = new byte[] { (byte) 0x08, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; + } + System.arraycopy(header, 0, packet, 0, header.length); + + // Send the packet. + int port = random.nextInt(65534) + 1; + Os.connect(s, address, port); + Os.write(s, packet, 0, packet.length); + + // Expect a reply. + StructPollfd pollfd = new StructPollfd(); + pollfd.events = (short) POLLIN; // "error: possible loss of precision" + pollfd.fd = s; + int ret = Os.poll(new StructPollfd[] { pollfd }, SOCKET_TIMEOUT_MS); + assertEquals("Expected reply after sending ping", 1, ret); + + byte[] reply = new byte[LENGTH]; + int read = Os.read(s, reply, 0, LENGTH); + assertEquals(LENGTH, read); + + // Find out what the kernel set the ICMP ID to. + InetSocketAddress local = (InetSocketAddress) Os.getsockname(s); + port = local.getPort(); + packet[4] = (byte) ((port >> 8) & 0xff); + packet[5] = (byte) (port & 0xff); + + // Check the contents. + if (packet[0] == (byte) 0x80) { + packet[0] = (byte) 0x81; + } else { + packet[0] = 0; + } + // Zero out the checksum in the reply so it matches the uninitialized checksum in packet. + reply[2] = reply[3] = 0; + MoreAsserts.assertEquals(packet, reply); + } + + // Writes data to out and checks that it appears identically on in. + private static void writeAndCheckData( + OutputStream out, InputStream in, byte[] data) throws IOException { + out.write(data, 0, data.length); + out.flush(); + + byte[] read = new byte[data.length]; + int bytesRead = 0, totalRead = 0; + do { + bytesRead = in.read(read, totalRead, read.length - totalRead); + totalRead += bytesRead; + } while (bytesRead >= 0 && totalRead < data.length); + assertEquals(totalRead, data.length); + MoreAsserts.assertEquals(data, read); + } + + private void checkTcpReflection(String to, String expectedFrom) throws IOException { + // Exercise TCP over the VPN by "connecting to ourselves". We open a server socket and a + // client socket, and connect the client socket to a remote host, with the port of the + // server socket. The PacketReflector reflects the packets, changing the source addresses + // but not the ports, so our client socket is connected to our server socket, though both + // sockets think their peers are on the "remote" IP address. + + // Open a listening socket. + ServerSocket listen = new ServerSocket(0, 10, InetAddress.getByName("::")); + + // Connect the client socket to it. + InetAddress toAddr = InetAddress.getByName(to); + Socket client = new Socket(); + try { + client.connect(new InetSocketAddress(toAddr, listen.getLocalPort()), SOCKET_TIMEOUT_MS); + if (expectedFrom == null) { + closeQuietly(listen); + closeQuietly(client); + fail("Expected connection to fail, but it succeeded."); + } + } catch (IOException e) { + if (expectedFrom != null) { + closeQuietly(listen); + fail("Expected connection to succeed, but it failed."); + } else { + // We expected the connection to fail, and it did, so there's nothing more to test. + return; + } + } + + // The connection succeeded, and we expected it to succeed. Send some data; if things are + // working, the data will be sent to the VPN, reflected by the PacketReflector, and arrive + // at our server socket. For good measure, send some data in the other direction. + Socket server = null; + try { + // Accept the connection on the server side. + listen.setSoTimeout(SOCKET_TIMEOUT_MS); + server = listen.accept(); + checkConnectionOwnerUidTcp(client); + checkConnectionOwnerUidTcp(server); + // Check that the source and peer addresses are as expected. + assertEquals(expectedFrom, client.getLocalAddress().getHostAddress()); + assertEquals(expectedFrom, server.getLocalAddress().getHostAddress()); + assertEquals( + new InetSocketAddress(toAddr, client.getLocalPort()), + server.getRemoteSocketAddress()); + assertEquals( + new InetSocketAddress(toAddr, server.getLocalPort()), + client.getRemoteSocketAddress()); + + // Now write some data. + final int LENGTH = 32768; + byte[] data = new byte[LENGTH]; + new Random().nextBytes(data); + + // Make sure our writes don't block or time out, because we're single-threaded and can't + // read and write at the same time. + server.setReceiveBufferSize(LENGTH * 2); + client.setSendBufferSize(LENGTH * 2); + client.setSoTimeout(SOCKET_TIMEOUT_MS); + server.setSoTimeout(SOCKET_TIMEOUT_MS); + + // Send some data from client to server, then from server to client. + writeAndCheckData(client.getOutputStream(), server.getInputStream(), data); + writeAndCheckData(server.getOutputStream(), client.getInputStream(), data); + } finally { + closeQuietly(listen); + closeQuietly(client); + closeQuietly(server); + } + } + + private void checkConnectionOwnerUidUdp(DatagramSocket s, boolean expectSuccess) { + final int expectedUid = expectSuccess ? Process.myUid() : INVALID_UID; + InetSocketAddress loc = new InetSocketAddress(s.getLocalAddress(), s.getLocalPort()); + InetSocketAddress rem = new InetSocketAddress(s.getInetAddress(), s.getPort()); + int uid = mCM.getConnectionOwnerUid(OsConstants.IPPROTO_UDP, loc, rem); + assertEquals(expectedUid, uid); + } + + private void checkConnectionOwnerUidTcp(Socket s) { + final int expectedUid = Process.myUid(); + InetSocketAddress loc = new InetSocketAddress(s.getLocalAddress(), s.getLocalPort()); + InetSocketAddress rem = new InetSocketAddress(s.getInetAddress(), s.getPort()); + int uid = mCM.getConnectionOwnerUid(OsConstants.IPPROTO_TCP, loc, rem); + assertEquals(expectedUid, uid); + } + + private void checkUdpEcho(String to, String expectedFrom) throws IOException { + DatagramSocket s; + InetAddress address = InetAddress.getByName(to); + if (address instanceof Inet6Address) { // http://b/18094870 + s = new DatagramSocket(0, InetAddress.getByName("::")); + } else { + s = new DatagramSocket(); + } + s.setSoTimeout(SOCKET_TIMEOUT_MS); + + Random random = new Random(); + byte[] data = new byte[random.nextInt(1650)]; + random.nextBytes(data); + DatagramPacket p = new DatagramPacket(data, data.length); + s.connect(address, 7); + + if (expectedFrom != null) { + assertEquals("Unexpected source address: ", + expectedFrom, s.getLocalAddress().getHostAddress()); + } + + try { + if (expectedFrom != null) { + s.send(p); + checkConnectionOwnerUidUdp(s, true); + s.receive(p); + MoreAsserts.assertEquals(data, p.getData()); + } else { + try { + s.send(p); + s.receive(p); + fail("Received unexpected reply"); + } catch (IOException expected) { + checkConnectionOwnerUidUdp(s, false); + } + } + } finally { + s.close(); + } + } + + private void checkTrafficOnVpn() throws Exception { + checkUdpEcho("192.0.2.251", "192.0.2.2"); + checkUdpEcho("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe"); + checkPing("2001:db8:dead:beef::f00"); + checkTcpReflection("192.0.2.252", "192.0.2.2"); + checkTcpReflection("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe"); + } + + private void checkNoTrafficOnVpn() throws Exception { + checkUdpEcho("192.0.2.251", null); + checkUdpEcho("2001:db8:dead:beef::f00", null); + checkTcpReflection("192.0.2.252", null); + checkTcpReflection("2001:db8:dead:beef::f00", null); + } + + private FileDescriptor openSocketFd(String host, int port, int timeoutMs) throws Exception { + Socket s = new Socket(host, port); + s.setSoTimeout(timeoutMs); + // Dup the filedescriptor so ParcelFileDescriptor's finalizer doesn't garbage collect it + // and cause our fd to become invalid. http://b/35927643 . + FileDescriptor fd = Os.dup(ParcelFileDescriptor.fromSocket(s).getFileDescriptor()); + s.close(); + return fd; + } + + private FileDescriptor openSocketFdInOtherApp( + String host, int port, int timeoutMs) throws Exception { + Log.d(TAG, String.format("Creating test socket in UID=%d, my UID=%d", + mRemoteSocketFactoryClient.getUid(), Os.getuid())); + FileDescriptor fd = mRemoteSocketFactoryClient.openSocketFd(host, port, TIMEOUT_MS); + return fd; + } + + private void sendRequest(FileDescriptor fd, String host) throws Exception { + String request = "GET /generate_204 HTTP/1.1\r\n" + + "Host: " + host + "\r\n" + + "Connection: keep-alive\r\n\r\n"; + byte[] requestBytes = request.getBytes(StandardCharsets.UTF_8); + int ret = Os.write(fd, requestBytes, 0, requestBytes.length); + Log.d(TAG, "Wrote " + ret + "bytes"); + + String expected = "HTTP/1.1 204 No Content\r\n"; + byte[] response = new byte[expected.length()]; + Os.read(fd, response, 0, response.length); + + String actual = new String(response, StandardCharsets.UTF_8); + assertEquals(expected, actual); + Log.d(TAG, "Got response: " + actual); + } + + private void assertSocketStillOpen(FileDescriptor fd, String host) throws Exception { + try { + assertTrue(fd.valid()); + sendRequest(fd, host); + assertTrue(fd.valid()); + } finally { + Os.close(fd); + } + } + + private void assertSocketClosed(FileDescriptor fd, String host) throws Exception { + try { + assertTrue(fd.valid()); + sendRequest(fd, host); + fail("Socket opened before VPN connects should be closed when VPN connects"); + } catch (ErrnoException expected) { + assertEquals(ECONNABORTED, expected.errno); + assertTrue(fd.valid()); + } finally { + Os.close(fd); + } + } + + private ContentResolver getContentResolver() { + return getInstrumentation().getContext().getContentResolver(); + } + + private boolean isPrivateDnsInStrictMode() { + return PRIVATE_DNS_MODE_PROVIDER_HOSTNAME.equals( + Settings.Global.getString(getContentResolver(), PRIVATE_DNS_MODE_SETTING)); + } + + private void storePrivateDnsSetting() { + mOldPrivateDnsMode = Settings.Global.getString(getContentResolver(), + PRIVATE_DNS_MODE_SETTING); + mOldPrivateDnsSpecifier = Settings.Global.getString(getContentResolver(), + PRIVATE_DNS_SPECIFIER_SETTING); + } + + private void restorePrivateDnsSetting() { + Settings.Global.putString(getContentResolver(), PRIVATE_DNS_MODE_SETTING, + mOldPrivateDnsMode); + Settings.Global.putString(getContentResolver(), PRIVATE_DNS_SPECIFIER_SETTING, + mOldPrivateDnsSpecifier); + } + + // TODO: replace with CtsNetUtils.awaitPrivateDnsSetting in Q or above. + private void expectPrivateDnsHostname(final String hostname) throws Exception { + final NetworkRequest request = new NetworkRequest.Builder() + .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) + .build(); + final CountDownLatch latch = new CountDownLatch(1); + final NetworkCallback callback = new NetworkCallback() { + @Override + public void onLinkPropertiesChanged(Network network, LinkProperties lp) { + if (network.equals(mNetwork) && + Objects.equals(lp.getPrivateDnsServerName(), hostname)) { + latch.countDown(); + } + } + }; + + mCM.registerNetworkCallback(request, callback); + + try { + assertTrue("Private DNS hostname was not " + hostname + " after " + TIMEOUT_MS + "ms", + latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + } finally { + mCM.unregisterNetworkCallback(callback); + } + } + + private void setAndVerifyPrivateDns(boolean strictMode) throws Exception { + final ContentResolver cr = getInstrumentation().getContext().getContentResolver(); + String privateDnsHostname; + + if (strictMode) { + privateDnsHostname = "vpncts-nx.metric.gstatic.com"; + Settings.Global.putString(cr, PRIVATE_DNS_SPECIFIER_SETTING, privateDnsHostname); + Settings.Global.putString(cr, PRIVATE_DNS_MODE_SETTING, + PRIVATE_DNS_MODE_PROVIDER_HOSTNAME); + } else { + Settings.Global.putString(cr, PRIVATE_DNS_MODE_SETTING, PRIVATE_DNS_MODE_OPPORTUNISTIC); + privateDnsHostname = null; + } + + expectPrivateDnsHostname(privateDnsHostname); + + String randomName = "vpncts-" + new Random().nextInt(1000000000) + "-ds.metric.gstatic.com"; + if (strictMode) { + // Strict mode private DNS is enabled. DNS lookups should fail, because the private DNS + // server name is invalid. + try { + InetAddress.getByName(randomName); + fail("VPN DNS lookup should fail with private DNS enabled"); + } catch (UnknownHostException expected) { + } + } else { + // Strict mode private DNS is disabled. DNS lookup should succeed, because the VPN + // provides no DNS servers, and thus DNS falls through to the default network. + assertNotNull("VPN DNS lookup should succeed with private DNS disabled", + InetAddress.getByName(randomName)); + } + } + + // Tests that strict mode private DNS is used on VPNs. + private void checkStrictModePrivateDns() throws Exception { + final boolean initialMode = isPrivateDnsInStrictMode(); + setAndVerifyPrivateDns(!initialMode); + setAndVerifyPrivateDns(initialMode); + } + + public void testDefault() throws Exception { + if (!supportedHardware()) return; + // If adb TCP port opened, this test may running by adb over network. + // All of socket would be destroyed in this test. So this test don't + // support adb over network, see b/119382723. + if (SystemProperties.getInt("persist.adb.tcp.port", -1) > -1 + || SystemProperties.getInt("service.adb.tcp.port", -1) > -1) { + Log.i(TAG, "adb is running over the network, so skip this test"); + return; + } + + final BlockingBroadcastReceiver receiver = new BlockingBroadcastReceiver( + getInstrumentation().getTargetContext(), MyVpnService.ACTION_ESTABLISHED); + receiver.register(); + + FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS); + + startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, + new String[] {"0.0.0.0/0", "::/0"}, + "", "", null, null /* underlyingNetworks */, false /* isAlwaysMetered */); + + final Intent intent = receiver.awaitForBroadcast(TimeUnit.MINUTES.toMillis(1)); + assertNotNull("Failed to receive broadcast from VPN service", intent); + assertFalse("Wrong VpnService#isAlwaysOn", + intent.getBooleanExtra(MyVpnService.EXTRA_ALWAYS_ON, true)); + assertFalse("Wrong VpnService#isLockdownEnabled", + intent.getBooleanExtra(MyVpnService.EXTRA_LOCKDOWN_ENABLED, true)); + + assertSocketClosed(fd, TEST_HOST); + + checkTrafficOnVpn(); + + checkStrictModePrivateDns(); + + receiver.unregisterQuietly(); + } + + public void testAppAllowed() throws Exception { + if (!supportedHardware()) return; + + FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS); + + // Shell app must not be put in here or it would kill the ADB-over-network use case + String allowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName; + startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, + new String[] {"192.0.2.0/24", "2001:db8::/32"}, + allowedApps, "", null, null /* underlyingNetworks */, false /* isAlwaysMetered */); + + assertSocketClosed(fd, TEST_HOST); + + checkTrafficOnVpn(); + + checkStrictModePrivateDns(); + } + + public void testAppDisallowed() throws Exception { + if (!supportedHardware()) return; + + FileDescriptor localFd = openSocketFd(TEST_HOST, 80, TIMEOUT_MS); + FileDescriptor remoteFd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS); + + String disallowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName; + // If adb TCP port opened, this test may running by adb over TCP. + // Add com.android.shell appllication into blacklist to exclude adb socket for VPN test, + // see b/119382723. + // Note: The test don't support running adb over network for root device + disallowedApps = disallowedApps + ",com.android.shell"; + Log.i(TAG, "Append shell app to disallowedApps: " + disallowedApps); + startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, + new String[] {"192.0.2.0/24", "2001:db8::/32"}, + "", disallowedApps, null, null /* underlyingNetworks */, + false /* isAlwaysMetered */); + + assertSocketStillOpen(localFd, TEST_HOST); + assertSocketStillOpen(remoteFd, TEST_HOST); + + checkNoTrafficOnVpn(); + } + + public void testGetConnectionOwnerUidSecurity() throws Exception { + if (!supportedHardware()) return; + + DatagramSocket s; + InetAddress address = InetAddress.getByName("localhost"); + s = new DatagramSocket(); + s.setSoTimeout(SOCKET_TIMEOUT_MS); + s.connect(address, 7); + InetSocketAddress loc = new InetSocketAddress(s.getLocalAddress(), s.getLocalPort()); + InetSocketAddress rem = new InetSocketAddress(s.getInetAddress(), s.getPort()); + try { + int uid = mCM.getConnectionOwnerUid(OsConstants.IPPROTO_TCP, loc, rem); + fail("Only an active VPN app may call this API."); + } catch (SecurityException expected) { + return; + } + } + + public void testSetProxy() throws Exception { + if (!supportedHardware()) return; + ProxyInfo initialProxy = mCM.getDefaultProxy(); + // Receiver for the proxy change broadcast. + BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver(); + proxyBroadcastReceiver.register(); + + String allowedApps = mPackageName; + ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("10.0.0.1", 8888); + startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, + new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", + testProxyInfo, null /* underlyingNetworks */, false /* isAlwaysMetered */); + + // Check that the proxy change broadcast is received + try { + assertNotNull("No proxy change was broadcast when VPN is connected.", + proxyBroadcastReceiver.awaitForBroadcast()); + } finally { + proxyBroadcastReceiver.unregisterQuietly(); + } + + // Proxy is set correctly in network and in link properties. + assertNetworkHasExpectedProxy(testProxyInfo, mNetwork); + assertDefaultProxy(testProxyInfo); + + proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver(); + proxyBroadcastReceiver.register(); + stopVpn(); + try { + assertNotNull("No proxy change was broadcast when VPN was disconnected.", + proxyBroadcastReceiver.awaitForBroadcast()); + } finally { + proxyBroadcastReceiver.unregisterQuietly(); + } + + // After disconnecting from VPN, the proxy settings are the ones of the initial network. + assertDefaultProxy(initialProxy); + } + + public void testSetProxyDisallowedApps() throws Exception { + if (!supportedHardware()) return; + ProxyInfo initialProxy = mCM.getDefaultProxy(); + + // If adb TCP port opened, this test may running by adb over TCP. + // Add com.android.shell appllication into blacklist to exclude adb socket for VPN test, + // see b/119382723. + // Note: The test don't support running adb over network for root device + String disallowedApps = mPackageName + ",com.android.shell"; + ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("10.0.0.1", 8888); + startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, + new String[] {"0.0.0.0/0", "::/0"}, "", disallowedApps, + testProxyInfo, null /* underlyingNetworks */, false /* isAlwaysMetered */); + + // The disallowed app does has the proxy configs of the default network. + assertNetworkHasExpectedProxy(initialProxy, mCM.getActiveNetwork()); + assertDefaultProxy(initialProxy); + } + + public void testNoProxy() throws Exception { + if (!supportedHardware()) return; + ProxyInfo initialProxy = mCM.getDefaultProxy(); + BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver(); + proxyBroadcastReceiver.register(); + String allowedApps = mPackageName; + startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, + new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null, + null /* underlyingNetworks */, false /* isAlwaysMetered */); + + try { + assertNotNull("No proxy change was broadcast.", + proxyBroadcastReceiver.awaitForBroadcast()); + } finally { + proxyBroadcastReceiver.unregisterQuietly(); + } + + // The VPN network has no proxy set. + assertNetworkHasExpectedProxy(null, mNetwork); + + proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver(); + proxyBroadcastReceiver.register(); + stopVpn(); + try { + assertNotNull("No proxy change was broadcast.", + proxyBroadcastReceiver.awaitForBroadcast()); + } finally { + proxyBroadcastReceiver.unregisterQuietly(); + } + // After disconnecting from VPN, the proxy settings are the ones of the initial network. + assertDefaultProxy(initialProxy); + assertNetworkHasExpectedProxy(initialProxy, mCM.getActiveNetwork()); + } + + public void testBindToNetworkWithProxy() throws Exception { + if (!supportedHardware()) return; + String allowedApps = mPackageName; + Network initialNetwork = mCM.getActiveNetwork(); + ProxyInfo initialProxy = mCM.getDefaultProxy(); + ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("10.0.0.1", 8888); + // Receiver for the proxy change broadcast. + BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver(); + proxyBroadcastReceiver.register(); + startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, + new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", + testProxyInfo, null /* underlyingNetworks */, false /* isAlwaysMetered */); + + assertDefaultProxy(testProxyInfo); + mCM.bindProcessToNetwork(initialNetwork); + try { + assertNotNull("No proxy change was broadcast.", + proxyBroadcastReceiver.awaitForBroadcast()); + } finally { + proxyBroadcastReceiver.unregisterQuietly(); + } + assertDefaultProxy(initialProxy); + } + + public void testVpnMeterednessWithNoUnderlyingNetwork() throws Exception { + if (!supportedHardware()) { + return; + } + // VPN is not routing any traffic i.e. its underlying networks is an empty array. + ArrayList underlyingNetworks = new ArrayList<>(); + String allowedApps = mPackageName; + + startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, + new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null, + underlyingNetworks, false /* isAlwaysMetered */); + + // VPN should now be the active network. + assertEquals(mNetwork, mCM.getActiveNetwork()); + assertVpnTransportContains(NetworkCapabilities.TRANSPORT_VPN); + // VPN with no underlying networks should be metered by default. + assertTrue(isNetworkMetered(mNetwork)); + assertTrue(mCM.isActiveNetworkMetered()); + } + + public void testVpnMeterednessWithNullUnderlyingNetwork() throws Exception { + if (!supportedHardware()) { + return; + } + Network underlyingNetwork = mCM.getActiveNetwork(); + if (underlyingNetwork == null) { + Log.i(TAG, "testVpnMeterednessWithNullUnderlyingNetwork cannot execute" + + " unless there is an active network"); + return; + } + // VPN tracks platform default. + ArrayList underlyingNetworks = null; + String allowedApps = mPackageName; + + startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, + new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null, + underlyingNetworks, false /*isAlwaysMetered */); + + // Ensure VPN transports contains underlying network's transports. + assertVpnTransportContains(underlyingNetwork); + // Its meteredness should be same as that of underlying network. + assertEquals(isNetworkMetered(underlyingNetwork), isNetworkMetered(mNetwork)); + // Meteredness based on VPN capabilities and CM#isActiveNetworkMetered should be in sync. + assertEquals(isNetworkMetered(mNetwork), mCM.isActiveNetworkMetered()); + } + + public void testVpnMeterednessWithNonNullUnderlyingNetwork() throws Exception { + if (!supportedHardware()) { + return; + } + Network underlyingNetwork = mCM.getActiveNetwork(); + if (underlyingNetwork == null) { + Log.i(TAG, "testVpnMeterednessWithNonNullUnderlyingNetwork cannot execute" + + " unless there is an active network"); + return; + } + // VPN explicitly declares WiFi to be its underlying network. + ArrayList underlyingNetworks = new ArrayList<>(1); + underlyingNetworks.add(underlyingNetwork); + String allowedApps = mPackageName; + + startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, + new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null, + underlyingNetworks, false /* isAlwaysMetered */); + + // Ensure VPN transports contains underlying network's transports. + assertVpnTransportContains(underlyingNetwork); + // Its meteredness should be same as that of underlying network. + assertEquals(isNetworkMetered(underlyingNetwork), isNetworkMetered(mNetwork)); + // Meteredness based on VPN capabilities and CM#isActiveNetworkMetered should be in sync. + assertEquals(isNetworkMetered(mNetwork), mCM.isActiveNetworkMetered()); + } + + public void testAlwaysMeteredVpnWithNullUnderlyingNetwork() throws Exception { + if (!supportedHardware()) { + return; + } + Network underlyingNetwork = mCM.getActiveNetwork(); + if (underlyingNetwork == null) { + Log.i(TAG, "testAlwaysMeteredVpnWithNullUnderlyingNetwork cannot execute" + + " unless there is an active network"); + return; + } + // VPN tracks platform default. + ArrayList underlyingNetworks = null; + String allowedApps = mPackageName; + boolean isAlwaysMetered = true; + + startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, + new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null, + underlyingNetworks, isAlwaysMetered); + + // VPN's meteredness does not depend on underlying network since it is always metered. + assertTrue(isNetworkMetered(mNetwork)); + assertTrue(mCM.isActiveNetworkMetered()); + } + + public void testAlwaysMeteredVpnWithNonNullUnderlyingNetwork() throws Exception { + if (!supportedHardware()) { + return; + } + Network underlyingNetwork = mCM.getActiveNetwork(); + if (underlyingNetwork == null) { + Log.i(TAG, "testAlwaysMeteredVpnWithNonNullUnderlyingNetwork cannot execute" + + " unless there is an active network"); + return; + } + // VPN explicitly declares its underlying network. + ArrayList underlyingNetworks = new ArrayList<>(1); + underlyingNetworks.add(underlyingNetwork); + String allowedApps = mPackageName; + boolean isAlwaysMetered = true; + + startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, + new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null, + underlyingNetworks, isAlwaysMetered); + + // VPN's meteredness does not depend on underlying network since it is always metered. + assertTrue(isNetworkMetered(mNetwork)); + assertTrue(mCM.isActiveNetworkMetered()); + } + + public void testB141603906() throws Exception { + if (!supportedHardware()) { + return; + } + final InetSocketAddress src = new InetSocketAddress(0); + final InetSocketAddress dst = new InetSocketAddress(0); + final int NUM_THREADS = 8; + final int NUM_SOCKETS = 5000; + final Thread[] threads = new Thread[NUM_THREADS]; + startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, + new String[] {"0.0.0.0/0", "::/0"}, + "" /* allowedApplications */, "com.android.shell" /* disallowedApplications */, + null /* proxyInfo */, null /* underlyingNetworks */, false /* isAlwaysMetered */); + + for (int i = 0; i < NUM_THREADS; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < NUM_SOCKETS; j++) { + mCM.getConnectionOwnerUid(IPPROTO_TCP, src, dst); + } + }); + } + for (Thread thread : threads) { + thread.start(); + } + for (Thread thread : threads) { + thread.join(); + } + stopVpn(); + } + + private boolean isNetworkMetered(Network network) { + NetworkCapabilities nc = mCM.getNetworkCapabilities(network); + return !nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED); + } + + private void assertVpnTransportContains(Network underlyingNetwork) { + int[] transports = mCM.getNetworkCapabilities(underlyingNetwork).getTransportTypes(); + assertVpnTransportContains(transports); + } + + private void assertVpnTransportContains(int... transports) { + NetworkCapabilities vpnCaps = mCM.getNetworkCapabilities(mNetwork); + for (int transport : transports) { + assertTrue(vpnCaps.hasTransport(transport)); + } + } + + private void assertDefaultProxy(ProxyInfo expected) { + assertEquals("Incorrect proxy config.", expected, mCM.getDefaultProxy()); + String expectedHost = expected == null ? null : expected.getHost(); + String expectedPort = expected == null ? null : String.valueOf(expected.getPort()); + assertEquals("Incorrect proxy host system property.", expectedHost, + System.getProperty("http.proxyHost")); + assertEquals("Incorrect proxy port system property.", expectedPort, + System.getProperty("http.proxyPort")); + } + + private void assertNetworkHasExpectedProxy(ProxyInfo expected, Network network) { + LinkProperties lp = mCM.getLinkProperties(network); + assertNotNull("The network link properties object is null.", lp); + assertEquals("Incorrect proxy config.", expected, lp.getHttpProxy()); + + assertEquals(expected, mCM.getProxyForNetwork(network)); + } + + class ProxyChangeBroadcastReceiver extends BlockingBroadcastReceiver { + private boolean received; + + public ProxyChangeBroadcastReceiver() { + super(VpnTest.this.getInstrumentation().getContext(), Proxy.PROXY_CHANGE_ACTION); + received = false; + } + + @Override + public void onReceive(Context context, Intent intent) { + if (!received) { + // Do not call onReceive() more than once. + super.onReceive(context, intent); + } + received = true; + } + } + + /** + * Verifies that DownloadManager has CONNECTIVITY_USE_RESTRICTED_NETWORKS permission that can + * bind socket to VPN when it is in VPN disallowed list but requested downloading app is in VPN + * allowed list. + * See b/165774987. + */ + public void testDownloadWithDownloadManagerDisallowed() throws Exception { + if (!supportedHardware()) return; + + // Start a VPN with DownloadManager package in disallowed list. + startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, + new String[] {"192.0.2.0/24", "2001:db8::/32"}, + "" /* allowedApps */, "com.android.providers.downloads", null /* proxyInfo */, + null /* underlyingNetworks */, false /* isAlwaysMetered */); + + final Context context = VpnTest.this.getInstrumentation().getContext(); + final DownloadManager dm = context.getSystemService(DownloadManager.class); + final DownloadCompleteReceiver receiver = new DownloadCompleteReceiver(); + try { + context.registerReceiver(receiver, + new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); + + // Enqueue a request and check only one download. + final long id = dm.enqueue(new Request(Uri.parse("https://www.google.com"))); + assertEquals(1, getTotalNumberDownloads(dm, new Query())); + assertEquals(1, getTotalNumberDownloads(dm, new Query().setFilterById(id))); + + // Wait for download complete and check status. + assertEquals(id, receiver.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals(1, getTotalNumberDownloads(dm, + new Query().setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL))); + + // Remove download. + assertEquals(1, dm.remove(id)); + assertEquals(0, getTotalNumberDownloads(dm, new Query())); + } finally { + context.unregisterReceiver(receiver); + } + } + + private static int getTotalNumberDownloads(final DownloadManager dm, final Query query) { + try (Cursor cursor = dm.query(query)) { return cursor.getCount(); } + } + + private static class DownloadCompleteReceiver extends BroadcastReceiver { + private final CompletableFuture future = new CompletableFuture<>(); + + @Override + public void onReceive(Context context, Intent intent) { + future.complete(intent.getLongExtra( + DownloadManager.EXTRA_DOWNLOAD_ID, -1 /* defaultValue */)); + } + + public long get(long timeout, TimeUnit unit) throws Exception { + return future.get(timeout, unit); + } + } +} diff --git a/tests/cts/hostside/app2/Android.bp b/tests/cts/hostside/app2/Android.bp new file mode 100644 index 0000000000..8e279311a8 --- /dev/null +++ b/tests/cts/hostside/app2/Android.bp @@ -0,0 +1,29 @@ +// +// 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. +// + +android_test_helper_app { + name: "CtsHostsideNetworkTestsApp2", + defaults: ["cts_support_defaults"], + sdk_version: "current", + static_libs: ["CtsHostsideNetworkTestsAidl"], + srcs: ["src/**/*.java"], + // Tag this module as a cts test artifact + test_suites: [ + "cts", + "general-tests", + ], + certificate: ":cts-net-app", +} diff --git a/tests/cts/hostside/app2/AndroidManifest.xml b/tests/cts/hostside/app2/AndroidManifest.xml new file mode 100644 index 0000000000..ad270b3170 --- /dev/null +++ b/tests/cts/hostside/app2/AndroidManifest.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/cts/hostside/app2/res/drawable/ic_notification.png b/tests/cts/hostside/app2/res/drawable/ic_notification.png new file mode 100644 index 0000000000..6ae570b4db Binary files /dev/null and b/tests/cts/hostside/app2/res/drawable/ic_notification.png differ diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java new file mode 100644 index 0000000000..351733edc5 --- /dev/null +++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java @@ -0,0 +1,94 @@ +/* + * 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.cts.net.hostside.app2; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; + +import com.android.cts.net.hostside.INetworkStateObserver; + +public final class Common { + + static final String TAG = "CtsNetApp2"; + + // Constants below must match values defined on app's + // AbstractRestrictBackgroundNetworkTestCase.java + static final String MANIFEST_RECEIVER = "ManifestReceiver"; + static final String DYNAMIC_RECEIVER = "DynamicReceiver"; + + static final String ACTION_RECEIVER_READY = + "com.android.cts.net.hostside.app2.action.RECEIVER_READY"; + static final String ACTION_FINISH_ACTIVITY = + "com.android.cts.net.hostside.app2.action.FINISH_ACTIVITY"; + static final String ACTION_SHOW_TOAST = + "com.android.cts.net.hostside.app2.action.SHOW_TOAST"; + + static final String NOTIFICATION_TYPE_CONTENT = "CONTENT"; + static final String NOTIFICATION_TYPE_DELETE = "DELETE"; + static final String NOTIFICATION_TYPE_FULL_SCREEN = "FULL_SCREEN"; + static final String NOTIFICATION_TYPE_BUNDLE = "BUNDLE"; + static final String NOTIFICATION_TYPE_ACTION = "ACTION"; + static final String NOTIFICATION_TYPE_ACTION_BUNDLE = "ACTION_BUNDLE"; + static final String NOTIFICATION_TYPE_ACTION_REMOTE_INPUT = "ACTION_REMOTE_INPUT"; + + static final String TEST_PKG = "com.android.cts.net.hostside"; + static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer"; + + static int getUid(Context context) { + final String packageName = context.getPackageName(); + try { + return context.getPackageManager().getPackageUid(packageName, 0); + } catch (NameNotFoundException e) { + throw new IllegalStateException("Could not get UID for " + packageName, e); + } + } + + static void notifyNetworkStateObserver(Context context, Intent intent) { + if (intent == null) { + return; + } + final Bundle extras = intent.getExtras(); + if (extras == null) { + return; + } + final INetworkStateObserver observer = INetworkStateObserver.Stub.asInterface( + extras.getBinder(KEY_NETWORK_STATE_OBSERVER)); + if (observer != null) { + try { + if (!observer.isForeground()) { + Log.e(TAG, "App didn't come to foreground"); + observer.onNetworkStateChecked(null); + return; + } + } catch (RemoteException e) { + Log.e(TAG, "Error occurred while reading the proc state: " + e); + } + AsyncTask.execute(() -> { + try { + observer.onNetworkStateChecked( + MyBroadcastReceiver.checkNetworkStatus(context)); + } catch (RemoteException e) { + Log.e(TAG, "Error occurred while notifying the observer: " + e); + } + }); + } + } +} diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java new file mode 100644 index 0000000000..286cc2fb56 --- /dev/null +++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java @@ -0,0 +1,75 @@ +/* + * 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.cts.net.hostside.app2; + +import static com.android.cts.net.hostside.app2.Common.ACTION_FINISH_ACTIVITY; +import static com.android.cts.net.hostside.app2.Common.TAG; +import static com.android.cts.net.hostside.app2.Common.TEST_PKG; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; + +import com.android.cts.net.hostside.INetworkStateObserver; + +/** + * Activity used to bring process to foreground. + */ +public class MyActivity extends Activity { + + private BroadcastReceiver finishCommandReceiver = null; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Log.d(TAG, "MyActivity.onCreate()"); + Common.notifyNetworkStateObserver(this, getIntent()); + finishCommandReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Log.d(TAG, "Finishing MyActivity"); + MyActivity.this.finish(); + } + }; + registerReceiver(finishCommandReceiver, new IntentFilter(ACTION_FINISH_ACTIVITY)); + } + + @Override + public void finish() { + if (finishCommandReceiver != null) { + unregisterReceiver(finishCommandReceiver); + } + super.finish(); + } + + @Override + protected void onStart() { + super.onStart(); + Log.d(TAG, "MyActivity.onStart()"); + } + + @Override + protected void onDestroy() { + Log.d(TAG, "MyActivity.onDestroy()"); + super.onDestroy(); + } +} diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java new file mode 100644 index 0000000000..aa54075783 --- /dev/null +++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java @@ -0,0 +1,267 @@ +/* + * 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.cts.net.hostside.app2; + +import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED; + +import static com.android.cts.net.hostside.app2.Common.ACTION_RECEIVER_READY; +import static com.android.cts.net.hostside.app2.Common.ACTION_SHOW_TOAST; +import static com.android.cts.net.hostside.app2.Common.MANIFEST_RECEIVER; +import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_ACTION; +import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_ACTION_BUNDLE; +import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_ACTION_REMOTE_INPUT; +import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_BUNDLE; +import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_CONTENT; +import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_DELETE; +import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_FULL_SCREEN; +import static com.android.cts.net.hostside.app2.Common.TAG; +import static com.android.cts.net.hostside.app2.Common.getUid; + +import android.app.Notification; +import android.app.Notification.Action; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.RemoteInput; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Bundle; +import android.util.Log; +import android.widget.Toast; + +import java.net.HttpURLConnection; +import java.net.URL; + +/** + * Receiver used to: + *

    + *
  1. Count number of {@code RESTRICT_BACKGROUND_CHANGED} broadcasts received. + *
  2. Show a toast. + *
+ */ +public class MyBroadcastReceiver extends BroadcastReceiver { + + private static final int NETWORK_TIMEOUT_MS = 5 * 1000; + + private final String mName; + + public MyBroadcastReceiver() { + this(MANIFEST_RECEIVER); + } + + MyBroadcastReceiver(String name) { + Log.d(TAG, "Constructing MyBroadcastReceiver named " + name); + mName = name; + } + + @Override + public void onReceive(Context context, Intent intent) { + Log.d(TAG, "onReceive() for " + mName + ": " + intent); + final String action = intent.getAction(); + switch (action) { + case ACTION_RESTRICT_BACKGROUND_CHANGED: + increaseCounter(context, action); + break; + case ACTION_RECEIVER_READY: + final String message = mName + " is ready to rumble"; + Log.d(TAG, message); + setResultData(message); + break; + case ACTION_SHOW_TOAST: + showToast(context); + break; + default: + Log.e(TAG, "received unexpected action: " + action); + } + } + + @Override + public String toString() { + return "[MyBroadcastReceiver: mName=" + mName + "]"; + } + + private void increaseCounter(Context context, String action) { + final SharedPreferences prefs = context.getApplicationContext() + .getSharedPreferences(mName, Context.MODE_PRIVATE); + final int value = prefs.getInt(action, 0) + 1; + Log.d(TAG, "increaseCounter('" + action + "'): setting '" + mName + "' to " + value); + prefs.edit().putInt(action, value).apply(); + } + + static int getCounter(Context context, String action, String receiverName) { + final SharedPreferences prefs = context.getSharedPreferences(receiverName, + Context.MODE_PRIVATE); + final int value = prefs.getInt(action, 0); + Log.d(TAG, "getCounter('" + action + "', '" + receiverName + "'): " + value); + return value; + } + + static String getRestrictBackgroundStatus(Context context) { + final ConnectivityManager cm = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + final int apiStatus = cm.getRestrictBackgroundStatus(); + Log.d(TAG, "getRestrictBackgroundStatus: returning " + apiStatus); + return String.valueOf(apiStatus); + } + + private static final String NETWORK_STATUS_TEMPLATE = "%s|%s|%s|%s|%s"; + /** + * Checks whether the network is available and return a string which can then be send as a + * result data for the ordered broadcast. + * + *

+ * The string has the following format: + * + *


+     * NetinfoState|NetinfoDetailedState|RealConnectionCheck|RealConnectionCheckDetails|Netinfo
+     * 
+ * + *

Where: + * + *

+ * + * For example, if the connection was established fine, the result would be something like: + *


+     * CONNECTED|CONNECTED|true|200|[type: WIFI[], state: CONNECTED/CONNECTED, reason: ...]
+     * 
+ * + */ + // TODO: now that it uses Binder, it counl return a Bundle with the data parts instead... + static String checkNetworkStatus(Context context) { + final ConnectivityManager cm = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + // TODO: connect to a hostside server instead + final String address = "http://example.com"; + final NetworkInfo networkInfo = cm.getActiveNetworkInfo(); + Log.d(TAG, "Running checkNetworkStatus() on thread " + + Thread.currentThread().getName() + " for UID " + getUid(context) + + "\n\tactiveNetworkInfo: " + networkInfo + "\n\tURL: " + address); + boolean checkStatus = false; + String checkDetails = "N/A"; + try { + final URL url = new URL(address); + final HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setReadTimeout(NETWORK_TIMEOUT_MS); + conn.setConnectTimeout(NETWORK_TIMEOUT_MS / 2); + conn.setRequestMethod("GET"); + conn.setDoInput(true); + conn.connect(); + final int response = conn.getResponseCode(); + checkStatus = true; + checkDetails = "HTTP response for " + address + ": " + response; + } catch (Exception e) { + checkStatus = false; + checkDetails = "Exception getting " + address + ": " + e; + } + Log.d(TAG, checkDetails); + final String state, detailedState; + if (networkInfo != null) { + state = networkInfo.getState().name(); + detailedState = networkInfo.getDetailedState().name(); + } else { + state = detailedState = "null"; + } + final String status = String.format(NETWORK_STATUS_TEMPLATE, state, detailedState, + Boolean.valueOf(checkStatus), checkDetails, networkInfo); + Log.d(TAG, "Offering " + status); + return status; + } + + /** + * Sends a system notification containing actions with pending intents to launch the app's + * main activitiy or service. + */ + static void sendNotification(Context context, String channelId, int notificationId, + String notificationType ) { + Log.d(TAG, "sendNotification: id=" + notificationId + ", type=" + notificationType); + final Intent serviceIntent = new Intent(context, MyService.class); + final PendingIntent pendingIntent = PendingIntent.getService(context, 0, serviceIntent, + notificationId); + final Bundle bundle = new Bundle(); + bundle.putCharSequence("parcelable", "I am not"); + + final Notification.Builder builder = new Notification.Builder(context, channelId) + .setSmallIcon(R.drawable.ic_notification); + + Action action = null; + switch (notificationType) { + case NOTIFICATION_TYPE_CONTENT: + builder + .setContentTitle("Light, Cameras...") + .setContentIntent(pendingIntent); + break; + case NOTIFICATION_TYPE_DELETE: + builder.setDeleteIntent(pendingIntent); + break; + case NOTIFICATION_TYPE_FULL_SCREEN: + builder.setFullScreenIntent(pendingIntent, true); + break; + case NOTIFICATION_TYPE_BUNDLE: + bundle.putParcelable("Magnum P.I. (Pending Intent)", pendingIntent); + builder.setExtras(bundle); + break; + case NOTIFICATION_TYPE_ACTION: + action = new Action.Builder( + R.drawable.ic_notification, "ACTION", pendingIntent) + .build(); + builder.addAction(action); + break; + case NOTIFICATION_TYPE_ACTION_BUNDLE: + bundle.putParcelable("Magnum A.P.I. (Action Pending Intent)", pendingIntent); + action = new Action.Builder( + R.drawable.ic_notification, "ACTION WITH BUNDLE", null) + .addExtras(bundle) + .build(); + builder.addAction(action); + break; + case NOTIFICATION_TYPE_ACTION_REMOTE_INPUT: + bundle.putParcelable("Magnum R.I. (Remote Input)", null); + final RemoteInput remoteInput = new RemoteInput.Builder("RI") + .addExtras(bundle) + .build(); + action = new Action.Builder( + R.drawable.ic_notification, "ACTION WITH REMOTE INPUT", pendingIntent) + .addRemoteInput(remoteInput) + .build(); + builder.addAction(action); + break; + default: + Log.e(TAG, "Unknown notification type: " + notificationType); + return; + } + + final Notification notification = builder.build(); + ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)) + .notify(notificationId, notification); + } + + private void showToast(Context context) { + Toast.makeText(context, "Toast from CTS test", Toast.LENGTH_SHORT).show(); + setResultData("Shown"); + } +} diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyForegroundService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyForegroundService.java new file mode 100644 index 0000000000..ff4ba656b1 --- /dev/null +++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyForegroundService.java @@ -0,0 +1,72 @@ +/* + * 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.cts.net.hostside.app2; + +import static com.android.cts.net.hostside.app2.Common.TAG; +import static com.android.cts.net.hostside.app2.Common.TEST_PKG; + +import android.R; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import com.android.cts.net.hostside.INetworkStateObserver; + +/** + * Service used to change app state to FOREGROUND_SERVICE. + */ +public class MyForegroundService extends Service { + private static final String NOTIFICATION_CHANNEL_ID = "cts/MyForegroundService"; + private static final int FLAG_START_FOREGROUND = 1; + private static final int FLAG_STOP_FOREGROUND = 2; + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.v(TAG, "MyForegroundService.onStartCommand(): " + intent); + NotificationManager notificationManager = getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(new NotificationChannel( + NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID, + NotificationManager.IMPORTANCE_DEFAULT)); + switch (intent.getFlags()) { + case FLAG_START_FOREGROUND: + Log.d(TAG, "Starting foreground"); + startForeground(42, new Notification.Builder(this, NOTIFICATION_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_dialog_alert) // any icon is fine + .build()); + Common.notifyNetworkStateObserver(this, intent); + break; + case FLAG_STOP_FOREGROUND: + Log.d(TAG, "Stopping foreground"); + stopForeground(true); + break; + default: + Log.wtf(TAG, "Invalid flag on intent " + intent); + } + return START_STICKY; + } +} diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java new file mode 100644 index 0000000000..590e17e5e5 --- /dev/null +++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java @@ -0,0 +1,193 @@ +/* + * 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.cts.net.hostside.app2; + +import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED; + +import static com.android.cts.net.hostside.app2.Common.ACTION_RECEIVER_READY; +import static com.android.cts.net.hostside.app2.Common.DYNAMIC_RECEIVER; +import static com.android.cts.net.hostside.app2.Common.TAG; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import com.android.cts.net.hostside.IMyService; +import com.android.cts.net.hostside.INetworkCallback; + +/** + * Service used to dynamically register a broadcast receiver. + */ +public class MyService extends Service { + private static final String NOTIFICATION_CHANNEL_ID = "MyService"; + + ConnectivityManager mCm; + + private MyBroadcastReceiver mReceiver; + private ConnectivityManager.NetworkCallback mNetworkCallback; + + // TODO: move MyBroadcast static functions here - they were kept there to make git diff easier. + + private IMyService.Stub mBinder = + new IMyService.Stub() { + + @Override + public void registerBroadcastReceiver() { + if (mReceiver != null) { + Log.d(TAG, "receiver already registered: " + mReceiver); + return; + } + final Context context = getApplicationContext(); + mReceiver = new MyBroadcastReceiver(DYNAMIC_RECEIVER); + context.registerReceiver(mReceiver, new IntentFilter(ACTION_RECEIVER_READY)); + context.registerReceiver(mReceiver, + new IntentFilter(ACTION_RESTRICT_BACKGROUND_CHANGED)); + Log.d(TAG, "receiver registered"); + } + + @Override + public int getCounters(String receiverName, String action) { + return MyBroadcastReceiver.getCounter(getApplicationContext(), action, receiverName); + } + + @Override + public String checkNetworkStatus() { + return MyBroadcastReceiver.checkNetworkStatus(getApplicationContext()); + } + + @Override + public String getRestrictBackgroundStatus() { + return MyBroadcastReceiver.getRestrictBackgroundStatus(getApplicationContext()); + } + + @Override + public void sendNotification(int notificationId, String notificationType) { + MyBroadcastReceiver .sendNotification(getApplicationContext(), NOTIFICATION_CHANNEL_ID, + notificationId, notificationType); + } + + @Override + public void registerNetworkCallback(INetworkCallback cb) { + if (mNetworkCallback != null) { + Log.d(TAG, "unregister previous network callback: " + mNetworkCallback); + unregisterNetworkCallback(); + } + Log.d(TAG, "registering network callback"); + + mNetworkCallback = new ConnectivityManager.NetworkCallback() { + @Override + public void onBlockedStatusChanged(Network network, boolean blocked) { + try { + cb.onBlockedStatusChanged(network, blocked); + } catch (RemoteException e) { + Log.d(TAG, "Cannot send onBlockedStatusChanged: " + e); + unregisterNetworkCallback(); + } + } + + @Override + public void onAvailable(Network network) { + try { + cb.onAvailable(network); + } catch (RemoteException e) { + Log.d(TAG, "Cannot send onAvailable: " + e); + unregisterNetworkCallback(); + } + } + + @Override + public void onLost(Network network) { + try { + cb.onLost(network); + } catch (RemoteException e) { + Log.d(TAG, "Cannot send onLost: " + e); + unregisterNetworkCallback(); + } + } + + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities cap) { + try { + cb.onCapabilitiesChanged(network, cap); + } catch (RemoteException e) { + Log.d(TAG, "Cannot send onCapabilitiesChanged: " + e); + unregisterNetworkCallback(); + } + } + }; + mCm.registerNetworkCallback(makeWifiNetworkRequest(), mNetworkCallback); + try { + cb.asBinder().linkToDeath(() -> unregisterNetworkCallback(), 0); + } catch (RemoteException e) { + unregisterNetworkCallback(); + } + } + + @Override + public void unregisterNetworkCallback() { + Log.d(TAG, "unregistering network callback"); + if (mNetworkCallback != null) { + mCm.unregisterNetworkCallback(mNetworkCallback); + mNetworkCallback = null; + } + } + }; + + private NetworkRequest makeWifiNetworkRequest() { + return new NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build(); + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + @Override + public void onCreate() { + final Context context = getApplicationContext(); + ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)) + .createNotificationChannel(new NotificationChannel(NOTIFICATION_CHANNEL_ID, + NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT)); + mCm = (ConnectivityManager) getApplicationContext() + .getSystemService(Context.CONNECTIVITY_SERVICE); + } + + @Override + public void onDestroy() { + final Context context = getApplicationContext(); + ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)) + .deleteNotificationChannel(NOTIFICATION_CHANNEL_ID); + if (mReceiver != null) { + Log.d(TAG, "onDestroy(): unregistering " + mReceiver); + getApplicationContext().unregisterReceiver(mReceiver); + } + + super.onDestroy(); + } +} diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/RemoteSocketFactoryService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/RemoteSocketFactoryService.java new file mode 100644 index 0000000000..b1b7d77ae1 --- /dev/null +++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/RemoteSocketFactoryService.java @@ -0,0 +1,63 @@ +/* + * 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.cts.net.hostside.app2; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.Process; +import android.util.Log; + +import com.android.cts.net.hostside.IRemoteSocketFactory; + +import java.net.Socket; + + +public class RemoteSocketFactoryService extends Service { + + private static final String TAG = RemoteSocketFactoryService.class.getSimpleName(); + + private IRemoteSocketFactory.Stub mBinder = new IRemoteSocketFactory.Stub() { + @Override + public ParcelFileDescriptor openSocketFd(String host, int port, int timeoutMs) { + try { + Socket s = new Socket(host, port); + s.setSoTimeout(timeoutMs); + return ParcelFileDescriptor.fromSocket(s); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + + @Override + public String getPackageName() { + return RemoteSocketFactoryService.this.getPackageName(); + } + + @Override + public int getUid() { + return Process.myUid(); + } + }; + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } +} diff --git a/tests/cts/hostside/certs/Android.bp b/tests/cts/hostside/certs/Android.bp new file mode 100644 index 0000000000..ab4cf340d0 --- /dev/null +++ b/tests/cts/hostside/certs/Android.bp @@ -0,0 +1,4 @@ +android_app_certificate { + name: "cts-net-app", + certificate: "cts-net-app", +} diff --git a/tests/cts/hostside/certs/README b/tests/cts/hostside/certs/README new file mode 100644 index 0000000000..b660a82dc8 --- /dev/null +++ b/tests/cts/hostside/certs/README @@ -0,0 +1,2 @@ +# Generated with: +development/tools/make_key cts-net-app '/CN=cts-net-app' diff --git a/tests/cts/hostside/certs/cts-net-app.pk8 b/tests/cts/hostside/certs/cts-net-app.pk8 new file mode 100644 index 0000000000..1703e4ee34 Binary files /dev/null and b/tests/cts/hostside/certs/cts-net-app.pk8 differ diff --git a/tests/cts/hostside/certs/cts-net-app.x509.pem b/tests/cts/hostside/certs/cts-net-app.x509.pem new file mode 100644 index 0000000000..a15ff48357 --- /dev/null +++ b/tests/cts/hostside/certs/cts-net-app.x509.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDAjCCAeqgAwIBAgIJAMhWwIIqr1r6MA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV +BAMMC2N0cy1uZXQtYXBwMB4XDTE4MDYyMDAyMjAwN1oXDTQ1MTEwNTAyMjAwN1ow +FjEUMBIGA1UEAwwLY3RzLW5ldC1hcHAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDefOayWQss1E+FQIONK6IhlXhe0BEyHshIrnPOOmuCPa/Svfbnmziy +hr1KTjaQ3ET/mGShwlt6AUti7nKx9aB71IJp5mSBuwW62A8jvN3yNOo45YV8+n1o +TrEoMWMf7hQmoOSqaSJ+VFuVms/kPSEh99okDgHCej6rsEkEcDoh6pJajQyUYDwR +SNAF8SrqCDhqFbZW/LWedvuikCUlNtzuv7/GrcLcsiWEfHv7UOBKpMjLo9BhD1XF +IefnxImcBQrQGMnE9TLixBiEeX5yauLgbZuxBqD/zsI2TH1FjxTeuJan83kLbqqH +FgyvPaUjwckAdQPyom7ZUYFnBc0LQ9xzAgMBAAGjUzBRMB0GA1UdDgQWBBRZrBEw +tAB2WNXj8dQ7ZOuJ34kY5DAfBgNVHSMEGDAWgBRZrBEwtAB2WNXj8dQ7ZOuJ34kY +5DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQDeI9AnLW6l/39y +z96w/ldxZVFPzBRiFIsJsPHVyXlD5vUHZv/ju2jFn8TZSZR5TK0bzCEoVLp34Sho +bbS0magP82yIvCRibyoyD+TDNnZkNJwjYnikE+/oyshTSQtpkn/rDA+0Y09BUC1E +N2I6bV9pTXLFg7oah2FmqPRPzhgeYUKENgOQkrrjUCn6y0i/k374n7aftzdniSIz +2kCRVEeN9gws6CnoMPx0vr32v/JVuPV6zfdJYadgj/eFRyTNE4msd9kE82Wc46eU +YiI+LuXZ3ZMUNWGY7MK2pOUUS52JsBQ3K235dA5WaU4x8OBlY/WkNYX/eLbNs5jj +FzLmhZZ1 +-----END CERTIFICATE----- diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java new file mode 100644 index 0000000000..1312085478 --- /dev/null +++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java @@ -0,0 +1,42 @@ +/* + * 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.cts.net; +public class HostsideNetworkCallbackTests extends HostsideNetworkTestCase { + + @Override + protected void setUp() throws Exception { + super.setUp(); + uninstallPackage(TEST_APP2_PKG, false); + installPackage(TEST_APP2_APK); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + uninstallPackage(TEST_APP2_PKG, true); + } + + public void testOnBlockedStatusChanged_dataSaver() throws Exception { + runDeviceTests(TEST_PKG, + TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_dataSaver"); + } + + public void testOnBlockedStatusChanged_powerSaver() throws Exception { + runDeviceTests(TEST_PKG, + TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_powerSaver"); + } +} + diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java new file mode 100644 index 0000000000..ce203795f9 --- /dev/null +++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java @@ -0,0 +1,186 @@ +/* + * 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.cts.net; + +import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; +import com.android.ddmlib.Log; +import com.android.ddmlib.testrunner.RemoteAndroidTestRunner; +import com.android.ddmlib.testrunner.TestResult.TestStatus; +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.result.CollectingTestListener; +import com.android.tradefed.result.TestDescription; +import com.android.tradefed.result.TestResult; +import com.android.tradefed.result.TestRunResult; +import com.android.tradefed.testtype.DeviceTestCase; +import com.android.tradefed.testtype.IAbi; +import com.android.tradefed.testtype.IAbiReceiver; +import com.android.tradefed.testtype.IBuildReceiver; + +import java.io.FileNotFoundException; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +abstract class HostsideNetworkTestCase extends DeviceTestCase implements IAbiReceiver, + IBuildReceiver { + protected static final boolean DEBUG = false; + protected static final String TAG = "HostsideNetworkTests"; + protected static final String TEST_PKG = "com.android.cts.net.hostside"; + protected static final String TEST_APK = "CtsHostsideNetworkTestsApp.apk"; + protected static final String TEST_APP2_PKG = "com.android.cts.net.hostside.app2"; + protected static final String TEST_APP2_APK = "CtsHostsideNetworkTestsApp2.apk"; + + private IAbi mAbi; + private IBuildInfo mCtsBuild; + + @Override + public void setAbi(IAbi abi) { + mAbi = abi; + } + + @Override + public void setBuild(IBuildInfo buildInfo) { + mCtsBuild = buildInfo; + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + assertNotNull(mAbi); + assertNotNull(mCtsBuild); + + uninstallPackage(TEST_PKG, false); + installPackage(TEST_APK); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + + uninstallPackage(TEST_PKG, true); + } + + protected void installPackage(String apk) throws FileNotFoundException, + DeviceNotAvailableException { + CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mCtsBuild); + assertNull(getDevice().installPackage(buildHelper.getTestFile(apk), + false /* reinstall */, true /* grantPermissions */)); + } + + protected void uninstallPackage(String packageName, boolean shouldSucceed) + throws DeviceNotAvailableException { + final String result = getDevice().uninstallPackage(packageName); + if (shouldSucceed) { + assertNull("uninstallPackage(" + packageName + ") failed: " + result, result); + } + } + + protected void assertPackageUninstalled(String packageName) throws DeviceNotAvailableException, + InterruptedException { + final String command = "cmd package list packages " + packageName; + final int max_tries = 5; + for (int i = 1; i <= max_tries; i++) { + final String result = runCommand(command); + if (result.trim().isEmpty()) { + return; + } + // 'list packages' filters by substring, so we need to iterate with the results + // and check one by one, otherwise 'com.android.cts.net.hostside' could return + // 'com.android.cts.net.hostside.app2' + boolean found = false; + for (String line : result.split("[\\r\\n]+")) { + if (line.endsWith(packageName)) { + found = true; + break; + } + } + if (!found) { + return; + } + i++; + Log.v(TAG, "Package " + packageName + " not uninstalled yet (" + result + + "); sleeping 1s before polling again"); + Thread.sleep(1000); + } + fail("Package '" + packageName + "' not uinstalled after " + max_tries + " seconds"); + } + + protected void runDeviceTests(String packageName, String testClassName) + throws DeviceNotAvailableException { + runDeviceTests(packageName, testClassName, null); + } + + protected void runDeviceTests(String packageName, String testClassName, String methodName) + throws DeviceNotAvailableException { + RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(packageName, + "androidx.test.runner.AndroidJUnitRunner", getDevice().getIDevice()); + + if (testClassName != null) { + if (methodName != null) { + testRunner.setMethodName(testClassName, methodName); + } else { + testRunner.setClassName(testClassName); + } + } + + final CollectingTestListener listener = new CollectingTestListener(); + getDevice().runInstrumentationTests(testRunner, listener); + + final TestRunResult result = listener.getCurrentRunResults(); + if (result.isRunFailure()) { + throw new AssertionError("Failed to successfully run device tests for " + + result.getName() + ": " + result.getRunFailureMessage()); + } + + if (result.hasFailedTests()) { + // build a meaningful error message + StringBuilder errorBuilder = new StringBuilder("on-device tests failed:\n"); + for (Map.Entry resultEntry : + result.getTestResults().entrySet()) { + if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) { + errorBuilder.append(resultEntry.getKey().toString()); + errorBuilder.append(":\n"); + errorBuilder.append(resultEntry.getValue().getStackTrace()); + } + } + throw new AssertionError(errorBuilder.toString()); + } + } + + private static final Pattern UID_PATTERN = + Pattern.compile(".*userId=([0-9]+)$", Pattern.MULTILINE); + + protected int getUid(String packageName) throws DeviceNotAvailableException { + final String output = runCommand("dumpsys package " + packageName); + final Matcher matcher = UID_PATTERN.matcher(output); + while (matcher.find()) { + final String match = matcher.group(1); + return Integer.parseInt(match); + } + throw new RuntimeException("Did not find regexp '" + UID_PATTERN + "' on adb output\n" + + output); + } + + protected String runCommand(String command) throws DeviceNotAvailableException { + Log.d(TAG, "Command: '" + command + "'"); + final String output = getDevice().executeShellCommand(command); + if (DEBUG) Log.v(TAG, "Output: " + output.trim()); + return output; + } +} diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java new file mode 100644 index 0000000000..ac28c7ab63 --- /dev/null +++ b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java @@ -0,0 +1,377 @@ +/* + * 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.cts.net; + +import com.android.ddmlib.Log; +import com.android.tradefed.device.DeviceNotAvailableException; + +public class HostsideRestrictBackgroundNetworkTests extends HostsideNetworkTestCase { + + @Override + protected void setUp() throws Exception { + super.setUp(); + + uninstallPackage(TEST_APP2_PKG, false); + installPackage(TEST_APP2_APK); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + + uninstallPackage(TEST_APP2_PKG, true); + } + + /************************** + * Data Saver Mode tests. * + **************************/ + + public void testDataSaverMode_disabled() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest", + "testGetRestrictBackgroundStatus_disabled"); + } + + public void testDataSaverMode_whitelisted() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest", + "testGetRestrictBackgroundStatus_whitelisted"); + } + + public void testDataSaverMode_enabled() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest", + "testGetRestrictBackgroundStatus_enabled"); + } + + public void testDataSaverMode_blacklisted() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest", + "testGetRestrictBackgroundStatus_blacklisted"); + } + + public void testDataSaverMode_reinstall() throws Exception { + final int oldUid = getUid(TEST_APP2_PKG); + + // Make sure whitelist is revoked when package is removed + addRestrictBackgroundWhitelist(oldUid); + + uninstallPackage(TEST_APP2_PKG, true); + assertPackageUninstalled(TEST_APP2_PKG); + assertRestrictBackgroundWhitelist(oldUid, false); + + installPackage(TEST_APP2_APK); + final int newUid = getUid(TEST_APP2_PKG); + assertRestrictBackgroundWhitelist(oldUid, false); + assertRestrictBackgroundWhitelist(newUid, false); + } + + public void testDataSaverMode_requiredWhitelistedPackages() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest", + "testGetRestrictBackgroundStatus_requiredWhitelistedPackages"); + } + + public void testDataSaverMode_broadcastNotSentOnUnsupportedDevices() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest", + "testBroadcastNotSentOnUnsupportedDevices"); + } + + /***************************** + * Battery Saver Mode tests. * + *****************************/ + + public void testBatterySaverModeMetered_disabled() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest", + "testBackgroundNetworkAccess_disabled"); + } + + public void testBatterySaverModeMetered_whitelisted() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest", + "testBackgroundNetworkAccess_whitelisted"); + } + + public void testBatterySaverModeMetered_enabled() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest", + "testBackgroundNetworkAccess_enabled"); + } + + public void testBatterySaverMode_reinstall() throws Exception { + if (!isDozeModeEnabled()) { + Log.w(TAG, "testBatterySaverMode_reinstall() skipped because device does not support " + + "Doze Mode"); + return; + } + + addPowerSaveModeWhitelist(TEST_APP2_PKG); + + uninstallPackage(TEST_APP2_PKG, true); + assertPackageUninstalled(TEST_APP2_PKG); + assertPowerSaveModeWhitelist(TEST_APP2_PKG, false); + + installPackage(TEST_APP2_APK); + assertPowerSaveModeWhitelist(TEST_APP2_PKG, false); + } + + public void testBatterySaverModeNonMetered_disabled() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest", + "testBackgroundNetworkAccess_disabled"); + } + + public void testBatterySaverModeNonMetered_whitelisted() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest", + "testBackgroundNetworkAccess_whitelisted"); + } + + public void testBatterySaverModeNonMetered_enabled() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest", + "testBackgroundNetworkAccess_enabled"); + } + + /******************* + * App idle tests. * + *******************/ + + public void testAppIdleMetered_disabled() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest", + "testBackgroundNetworkAccess_disabled"); + } + + public void testAppIdleMetered_whitelisted() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest", + "testBackgroundNetworkAccess_whitelisted"); + } + + public void testAppIdleMetered_tempWhitelisted() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest", + "testBackgroundNetworkAccess_tempWhitelisted"); + } + + public void testAppIdleMetered_enabled() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest", + "testBackgroundNetworkAccess_enabled"); + } + + public void testAppIdleMetered_idleWhitelisted() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest", + "testAppIdleNetworkAccess_idleWhitelisted"); + } + + // TODO: currently power-save mode and idle uses the same whitelist, so this test would be + // redundant (as it would be testing the same as testBatterySaverMode_reinstall()) + // public void testAppIdle_reinstall() throws Exception { + // } + + public void testAppIdleNonMetered_disabled() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest", + "testBackgroundNetworkAccess_disabled"); + } + + public void testAppIdleNonMetered_whitelisted() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest", + "testBackgroundNetworkAccess_whitelisted"); + } + + public void testAppIdleNonMetered_tempWhitelisted() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest", + "testBackgroundNetworkAccess_tempWhitelisted"); + } + + public void testAppIdleNonMetered_enabled() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest", + "testBackgroundNetworkAccess_enabled"); + } + + public void testAppIdleNonMetered_idleWhitelisted() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest", + "testAppIdleNetworkAccess_idleWhitelisted"); + } + + public void testAppIdleNonMetered_whenCharging() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest", + "testAppIdleNetworkAccess_whenCharging"); + } + + public void testAppIdleMetered_whenCharging() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest", + "testAppIdleNetworkAccess_whenCharging"); + } + + public void testAppIdle_toast() throws Exception { + // Check that showing a toast doesn't bring an app out of standby + runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest", + "testAppIdle_toast"); + } + + /******************** + * Doze Mode tests. * + ********************/ + + public void testDozeModeMetered_disabled() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest", + "testBackgroundNetworkAccess_disabled"); + } + + public void testDozeModeMetered_whitelisted() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest", + "testBackgroundNetworkAccess_whitelisted"); + } + + public void testDozeModeMetered_enabled() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest", + "testBackgroundNetworkAccess_enabled"); + } + + public void testDozeModeMetered_enabledButWhitelistedOnNotificationAction() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest", + "testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction"); + } + + // TODO: currently power-save mode and idle uses the same whitelist, so this test would be + // redundant (as it would be testing the same as testBatterySaverMode_reinstall()) + // public void testDozeMode_reinstall() throws Exception { + // } + + public void testDozeModeNonMetered_disabled() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest", + "testBackgroundNetworkAccess_disabled"); + } + + public void testDozeModeNonMetered_whitelisted() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest", + "testBackgroundNetworkAccess_whitelisted"); + } + + public void testDozeModeNonMetered_enabled() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest", + "testBackgroundNetworkAccess_enabled"); + } + + public void testDozeModeNonMetered_enabledButWhitelistedOnNotificationAction() + throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest", + "testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction"); + } + + /********************** + * Mixed modes tests. * + **********************/ + + public void testDataAndBatterySaverModes_meteredNetwork() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest", + "testDataAndBatterySaverModes_meteredNetwork"); + } + + public void testDataAndBatterySaverModes_nonMeteredNetwork() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest", + "testDataAndBatterySaverModes_nonMeteredNetwork"); + } + + public void testDozeAndBatterySaverMode_powerSaveWhitelists() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest", + "testDozeAndBatterySaverMode_powerSaveWhitelists"); + } + + public void testDozeAndAppIdle_powerSaveWhitelists() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest", + "testDozeAndAppIdle_powerSaveWhitelists"); + } + + public void testAppIdleAndDoze_tempPowerSaveWhitelists() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest", + "testAppIdleAndDoze_tempPowerSaveWhitelists"); + } + + public void testAppIdleAndBatterySaver_tempPowerSaveWhitelists() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest", + "testAppIdleAndBatterySaver_tempPowerSaveWhitelists"); + } + + public void testDozeAndAppIdle_appIdleWhitelist() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest", + "testDozeAndAppIdle_appIdleWhitelist"); + } + + public void testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest", + "testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists"); + } + + public void testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest", + "testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists"); + } + + /******************* + * Helper methods. * + *******************/ + + private void assertRestrictBackgroundWhitelist(int uid, boolean expected) throws Exception { + final int max_tries = 5; + boolean actual = false; + for (int i = 1; i <= max_tries; i++) { + final String output = runCommand("cmd netpolicy list restrict-background-whitelist "); + actual = output.contains(Integer.toString(uid)); + if (expected == actual) { + return; + } + Log.v(TAG, "whitelist check for uid " + uid + " doesn't match yet (expected " + + expected + ", got " + actual + "); sleeping 1s before polling again"); + Thread.sleep(1000); + } + fail("whitelist check for uid " + uid + " failed: expected " + + expected + ", got " + actual); + } + + private void assertPowerSaveModeWhitelist(String packageName, boolean expected) + throws Exception { + // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll + // need to use netpolicy for whitelisting + assertDelayedCommand("dumpsys deviceidle whitelist =" + packageName, + Boolean.toString(expected)); + } + + /** + * Asserts the result of a command, wait and re-running it a couple times if necessary. + */ + private void assertDelayedCommand(String command, String expectedResult) + throws InterruptedException, DeviceNotAvailableException { + final int maxTries = 5; + for (int i = 1; i <= maxTries; i++) { + final String result = runCommand(command).trim(); + if (result.equals(expectedResult)) return; + Log.v(TAG, "Command '" + command + "' returned '" + result + " instead of '" + + expectedResult + "' on attempt #; sleeping 1s before polling again"); + Thread.sleep(1000); + } + fail("Command '" + command + "' did not return '" + expectedResult + "' after " + maxTries + + " attempts"); + } + + protected void addRestrictBackgroundWhitelist(int uid) throws Exception { + runCommand("cmd netpolicy add restrict-background-whitelist " + uid); + assertRestrictBackgroundWhitelist(uid, true); + } + + private void addPowerSaveModeWhitelist(String packageName) throws Exception { + Log.i(TAG, "Adding package " + packageName + " to power-save-mode whitelist"); + // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll + // need to use netpolicy for whitelisting + runCommand("dumpsys deviceidle whitelist +" + packageName); + assertPowerSaveModeWhitelist(packageName, true); + } + + protected boolean isDozeModeEnabled() throws Exception { + final String result = runCommand("cmd deviceidle enabled deep").trim(); + return result.equals("1"); + } +} diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java new file mode 100644 index 0000000000..49b5f9dc96 --- /dev/null +++ b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java @@ -0,0 +1,103 @@ +/* + * 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.cts.net; + +public class HostsideVpnTests extends HostsideNetworkTestCase { + + @Override + protected void setUp() throws Exception { + super.setUp(); + + uninstallPackage(TEST_APP2_PKG, false); + installPackage(TEST_APP2_APK); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + + uninstallPackage(TEST_APP2_PKG, true); + } + + public void testDefault() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testDefault"); + } + + public void testAppAllowed() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testAppAllowed"); + } + + public void testAppDisallowed() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testAppDisallowed"); + } + + public void testGetConnectionOwnerUidSecurity() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testGetConnectionOwnerUidSecurity"); + } + + public void testSetProxy() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testSetProxy"); + } + + public void testSetProxyDisallowedApps() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testSetProxyDisallowedApps"); + } + + public void testNoProxy() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testNoProxy"); + } + + public void testBindToNetworkWithProxy() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testBindToNetworkWithProxy"); + } + + public void testVpnMeterednessWithNoUnderlyingNetwork() throws Exception { + runDeviceTests( + TEST_PKG, TEST_PKG + ".VpnTest", "testVpnMeterednessWithNoUnderlyingNetwork"); + } + + public void testVpnMeterednessWithNullUnderlyingNetwork() throws Exception { + runDeviceTests( + TEST_PKG, TEST_PKG + ".VpnTest", "testVpnMeterednessWithNullUnderlyingNetwork"); + } + + public void testVpnMeterednessWithNonNullUnderlyingNetwork() throws Exception { + runDeviceTests( + TEST_PKG, TEST_PKG + ".VpnTest", "testVpnMeterednessWithNonNullUnderlyingNetwork"); + } + + public void testAlwaysMeteredVpnWithNullUnderlyingNetwork() throws Exception { + runDeviceTests( + TEST_PKG, TEST_PKG + ".VpnTest", "testAlwaysMeteredVpnWithNullUnderlyingNetwork"); + } + + public void testAlwaysMeteredVpnWithNonNullUnderlyingNetwork() throws Exception { + runDeviceTests( + TEST_PKG, + TEST_PKG + ".VpnTest", + "testAlwaysMeteredVpnWithNonNullUnderlyingNetwork"); + } + + public void testB141603906() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testB141603906"); + } + + public void testDownloadWithDownloadManagerDisallowed() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", + "testDownloadWithDownloadManagerDisallowed"); + } +} diff --git a/tests/cts/hostside/src/com/android/cts/net/NetworkPolicyTestsPreparer.java b/tests/cts/hostside/src/com/android/cts/net/NetworkPolicyTestsPreparer.java new file mode 100644 index 0000000000..23aca24788 --- /dev/null +++ b/tests/cts/hostside/src/com/android/cts/net/NetworkPolicyTestsPreparer.java @@ -0,0 +1,92 @@ +/* + * 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.cts.net; + +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.invoker.TestInformation; +import com.android.tradefed.log.LogUtil; +import com.android.tradefed.targetprep.ITargetPreparer; + +public class NetworkPolicyTestsPreparer implements ITargetPreparer { + private ITestDevice mDevice; + private boolean mOriginalAirplaneModeEnabled; + private String mOriginalAppStandbyEnabled; + private String mOriginalBatteryStatsConstants; + private final static String KEY_STABLE_CHARGING_DELAY_MS = "battery_charged_delay_ms"; + private final static int DESIRED_STABLE_CHARGING_DELAY_MS = 0; + + @Override + public void setUp(TestInformation testInformation) throws DeviceNotAvailableException { + mDevice = testInformation.getDevice(); + mOriginalAppStandbyEnabled = getAppStandbyEnabled(); + setAppStandbyEnabled("1"); + LogUtil.CLog.d("Original app_standby_enabled: " + mOriginalAppStandbyEnabled); + + mOriginalBatteryStatsConstants = getBatteryStatsConstants(); + setBatteryStatsConstants( + KEY_STABLE_CHARGING_DELAY_MS + "=" + DESIRED_STABLE_CHARGING_DELAY_MS); + LogUtil.CLog.d("Original battery_saver_constants: " + mOriginalBatteryStatsConstants); + + mOriginalAirplaneModeEnabled = getAirplaneModeEnabled(); + // Turn off airplane mode in case another test left the device in that state. + setAirplaneModeEnabled(false); + LogUtil.CLog.d("Original airplane mode state: " + mOriginalAirplaneModeEnabled); + } + + @Override + public void tearDown(TestInformation testInformation, Throwable e) + throws DeviceNotAvailableException { + setAirplaneModeEnabled(mOriginalAirplaneModeEnabled); + setAppStandbyEnabled(mOriginalAppStandbyEnabled); + setBatteryStatsConstants(mOriginalBatteryStatsConstants); + } + + private void setAirplaneModeEnabled(boolean enable) throws DeviceNotAvailableException { + executeCmd("cmd connectivity airplane-mode " + (enable ? "enable" : "disable")); + } + + private boolean getAirplaneModeEnabled() throws DeviceNotAvailableException { + return "enabled".equals(executeCmd("cmd connectivity airplane-mode").trim()); + } + + private void setAppStandbyEnabled(String appStandbyEnabled) throws DeviceNotAvailableException { + if ("null".equals(appStandbyEnabled)) { + executeCmd("settings delete global app_standby_enabled"); + } else { + executeCmd("settings put global app_standby_enabled " + appStandbyEnabled); + } + } + + private String getAppStandbyEnabled() throws DeviceNotAvailableException { + return executeCmd("settings get global app_standby_enabled").trim(); + } + + private void setBatteryStatsConstants(String batteryStatsConstants) + throws DeviceNotAvailableException { + executeCmd("settings put global battery_stats_constants \"" + batteryStatsConstants + "\""); + } + + private String getBatteryStatsConstants() throws DeviceNotAvailableException { + return executeCmd("settings get global battery_stats_constants"); + } + + private String executeCmd(String cmd) throws DeviceNotAvailableException { + final String output = mDevice.executeShellCommand(cmd).trim(); + LogUtil.CLog.d("Output for '%s': %s", cmd, output); + return output; + } +} diff --git a/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java new file mode 100644 index 0000000000..19e61c62a0 --- /dev/null +++ b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java @@ -0,0 +1,169 @@ +/* + * 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.security.cts; + +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.testtype.DeviceTestCase; +import com.android.tradefed.testtype.IBuildReceiver; +import com.android.tradefed.testtype.IDeviceTest; + +import java.lang.Integer; +import java.lang.String; +import java.util.Arrays; +import java.util.List; +import java.util.ArrayList; + +/** + * Host-side tests for values in /proc/net. + * + * These tests analyze /proc/net to verify that certain networking properties are correct. + */ +public class ProcNetTest extends DeviceTestCase implements IBuildReceiver, IDeviceTest { + private static final String SPI_TIMEOUT_SYSCTL = "/proc/sys/net/core/xfrm_acq_expires"; + private static final int MIN_ACQ_EXPIRES = 3600; + // Global sysctls. Must be present and set to 1. + private static final String[] GLOBAL_SYSCTLS = { + "/proc/sys/net/ipv4/fwmark_reflect", + "/proc/sys/net/ipv6/fwmark_reflect", + "/proc/sys/net/ipv4/tcp_fwmark_accept", + }; + + // Per-interface IPv6 autoconf sysctls. + private static final String IPV6_SYSCTL_DIR = "/proc/sys/net/ipv6/conf"; + private static final String AUTOCONF_SYSCTL = "accept_ra_rt_table"; + + // Expected values for MIN|MAX_PLEN. + private static final String ACCEPT_RA_RT_INFO_MIN_PLEN_STRING = "accept_ra_rt_info_min_plen"; + private static final int ACCEPT_RA_RT_INFO_MIN_PLEN_VALUE = 48; + private static final String ACCEPT_RA_RT_INFO_MAX_PLEN_STRING = "accept_ra_rt_info_max_plen"; + private static final int ACCEPT_RA_RT_INFO_MAX_PLEN_VALUE = 64; + // Expected values for RFC 7559 router soliciations. + // Maximum number of router solicitations to send. -1 means no limit. + private static final int IPV6_WIFI_ROUTER_SOLICITATIONS = -1; + private ITestDevice mDevice; + private IBuildInfo mBuild; + private String[] mSysctlDirs; + + /** + * {@inheritDoc} + */ + @Override + public void setBuild(IBuildInfo build) { + mBuild = build; + } + + /** + * {@inheritDoc} + */ + @Override + public void setDevice(ITestDevice device) { + super.setDevice(device); + mDevice = device; + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + mSysctlDirs = getSysctlDirs(); + } + + private String[] getSysctlDirs() throws Exception { + String interfaceDirs[] = mDevice.executeAdbCommand("shell", "ls", "-1", + IPV6_SYSCTL_DIR).split("\n"); + List interfaceDirsList = new ArrayList(Arrays.asList(interfaceDirs)); + interfaceDirsList.remove("all"); + interfaceDirsList.remove("lo"); + return interfaceDirsList.toArray(new String[interfaceDirsList.size()]); + } + + + protected void assertLess(String sysctl, int a, int b) { + assertTrue("value of " + sysctl + ": expected < " + b + " but was: " + a, a < b); + } + + protected void assertAtLeast(String sysctl, int a, int b) { + assertTrue("value of " + sysctl + ": expected >= " + b + " but was: " + a, a >= b); + } + + public int readIntFromPath(String path) throws Exception { + String mode = mDevice.executeAdbCommand("shell", "stat", "-c", "%a", path).trim(); + String user = mDevice.executeAdbCommand("shell", "stat", "-c", "%u", path).trim(); + String group = mDevice.executeAdbCommand("shell", "stat", "-c", "%g", path).trim(); + assertEquals(mode, "644"); + assertEquals(user, "0"); + assertEquals(group, "0"); + return Integer.parseInt(mDevice.executeAdbCommand("shell", "cat", path).trim()); + } + + /** + * Checks that SPI default timeouts are overridden, and set to a reasonable length of time + */ + public void testMinAcqExpires() throws Exception { + int value = readIntFromPath(SPI_TIMEOUT_SYSCTL); + assertAtLeast(SPI_TIMEOUT_SYSCTL, value, MIN_ACQ_EXPIRES); + } + + /** + * Checks that the sysctls for multinetwork kernel features are present and + * enabled. + */ + public void testProcSysctls() throws Exception { + for (String sysctl : GLOBAL_SYSCTLS) { + int value = readIntFromPath(sysctl); + assertEquals(sysctl, 1, value); + } + + for (String interfaceDir : mSysctlDirs) { + String path = IPV6_SYSCTL_DIR + "/" + interfaceDir + "/" + AUTOCONF_SYSCTL; + int value = readIntFromPath(path); + assertLess(path, value, 0); + } + } + + /** + * Verify that accept_ra_rt_info_{min,max}_plen exists and is set to the expected value + */ + public void testAcceptRaRtInfoMinMaxPlen() throws Exception { + for (String interfaceDir : mSysctlDirs) { + String path = IPV6_SYSCTL_DIR + "/" + interfaceDir + "/" + "accept_ra_rt_info_min_plen"; + int value = readIntFromPath(path); + assertEquals(path, value, ACCEPT_RA_RT_INFO_MIN_PLEN_VALUE); + path = IPV6_SYSCTL_DIR + "/" + interfaceDir + "/" + "accept_ra_rt_info_max_plen"; + value = readIntFromPath(path); + assertEquals(path, value, ACCEPT_RA_RT_INFO_MAX_PLEN_VALUE); + } + } + + /** + * Verify that router_solicitations exists and is set to the expected value + * and verify that router_solicitation_max_interval exists and is in an acceptable interval. + */ + public void testRouterSolicitations() throws Exception { + for (String interfaceDir : mSysctlDirs) { + String path = IPV6_SYSCTL_DIR + "/" + interfaceDir + "/" + "router_solicitations"; + int value = readIntFromPath(path); + assertEquals(IPV6_WIFI_ROUTER_SOLICITATIONS, value); + path = IPV6_SYSCTL_DIR + "/" + interfaceDir + "/" + "router_solicitation_max_interval"; + int interval = readIntFromPath(path); + final int lowerBoundSec = 15 * 60; + final int upperBoundSec = 60 * 60; + assertTrue(lowerBoundSec <= interval); + assertTrue(interval <= upperBoundSec); + } + } +} diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp new file mode 100644 index 0000000000..528171a036 --- /dev/null +++ b/tests/cts/net/Android.bp @@ -0,0 +1,84 @@ +// Copyright (C) 2008 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. + +java_defaults { + name: "CtsNetTestCasesDefaults", + defaults: ["cts_defaults"], + + // Include both the 32 and 64 bit versions + compile_multilib: "both", + + libs: [ + "voip-common", + "android.test.base", + ], + + jni_libs: [ + "libcts_jni", + "libnativedns_jni", + "libnativemultinetwork_jni", + "libnativehelper_compat_libc++", + ], + + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + jarjar_rules: "jarjar-rules-shared.txt", + static_libs: [ + "FrameworksNetCommonTests", + "TestNetworkStackLib", + "core-tests-support", + "cts-net-utils", + "ctstestrunner-axt", + "junit", + "junit-params", + "net-utils-framework-common", + "truth-prebuilt", + ], + + // uncomment when b/13249961 is fixed + // sdk_version: "current", + platform_apis: true, +} + +// Networking CTS tests for development and release. These tests always target the platform SDK +// version, and are subject to all the restrictions appropriate to that version. Before SDK +// finalization, these tests have a min_sdk_version of 10000, and cannot be installed on release +// devices. +android_test { + name: "CtsNetTestCases", + defaults: ["CtsNetTestCasesDefaults"], + test_suites: [ + "cts", + "general-tests", + ], + test_config_template: "AndroidTestTemplate.xml", +} + +// Networking CTS tests that target the latest released SDK. These tests can be installed on release +// devices at any point in the Android release cycle and are useful for qualifying mainline modules +// on release devices. +android_test { + name: "CtsNetTestCasesLatestSdk", + defaults: ["CtsNetTestCasesDefaults"], + jni_uses_sdk_apis: true, + min_sdk_version: "29", + target_sdk_version: "30", + test_suites: [ + "general-tests", + "mts", + ], + test_config_template: "AndroidTestTemplate.xml", +} diff --git a/tests/cts/net/AndroidManifest.xml b/tests/cts/net/AndroidManifest.xml new file mode 100644 index 0000000000..a7e2bd780a --- /dev/null +++ b/tests/cts/net/AndroidManifest.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml new file mode 100644 index 0000000000..78a01e29c1 --- /dev/null +++ b/tests/cts/net/AndroidTestTemplate.xml @@ -0,0 +1,35 @@ + + + diff --git a/tests/cts/net/OWNERS b/tests/cts/net/OWNERS new file mode 100644 index 0000000000..d55855650f --- /dev/null +++ b/tests/cts/net/OWNERS @@ -0,0 +1,3 @@ +# Bug component: 31808 +lorenzo@google.com +satk@google.com diff --git a/tests/cts/net/TEST_MAPPING b/tests/cts/net/TEST_MAPPING new file mode 100644 index 0000000000..7545cb0c30 --- /dev/null +++ b/tests/cts/net/TEST_MAPPING @@ -0,0 +1,23 @@ +{ + // TODO: move to mainline-presubmit once supported + "presubmit": [ + { + "name": "CtsNetTestCasesLatestSdk", + "options": [ + { + "exclude-annotation": "com.android.testutils.SkipPresubmit" + } + ] + } + ], + "mainline-presubmit": [ + { + "name": "CtsNetTestCasesLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]", + "options": [ + { + "exclude-annotation": "com.android.testutils.SkipPresubmit" + } + ] + } + ] +} diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp new file mode 100644 index 0000000000..e43a5e82d0 --- /dev/null +++ b/tests/cts/net/api23Test/Android.bp @@ -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. + +android_test { + name: "CtsNetApi23TestCases", + defaults: ["cts_defaults"], + + // Include both the 32 and 64 bit versions + compile_multilib: "both", + + libs: [ + "android.test.base", + ], + + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + + static_libs: [ + "core-tests-support", + "compatibility-device-util-axt", + "cts-net-utils", + "ctstestrunner-axt", + "ctstestserver", + "mockwebserver", + "junit", + "junit-params", + "truth-prebuilt", + ], + + platform_apis: true, + + // Tag this module as a cts test artifact + test_suites: [ + "cts", + "general-tests", + ], + +} diff --git a/tests/cts/net/api23Test/AndroidManifest.xml b/tests/cts/net/api23Test/AndroidManifest.xml new file mode 100644 index 0000000000..4889660b97 --- /dev/null +++ b/tests/cts/net/api23Test/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/cts/net/api23Test/AndroidTest.xml b/tests/cts/net/api23Test/AndroidTest.xml new file mode 100644 index 0000000000..8042d5067d --- /dev/null +++ b/tests/cts/net/api23Test/AndroidTest.xml @@ -0,0 +1,31 @@ + + + diff --git a/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityManagerApi23Test.java b/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityManagerApi23Test.java new file mode 100644 index 0000000000..cdb66e3d5a --- /dev/null +++ b/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityManagerApi23Test.java @@ -0,0 +1,132 @@ +/* + * 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.cts.api23test; + +import static android.content.pm.PackageManager.FEATURE_WIFI; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.cts.util.CtsNetUtils; +import android.os.Looper; +import android.test.AndroidTestCase; +import android.util.Log; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +public class ConnectivityManagerApi23Test extends AndroidTestCase { + private static final String TAG = ConnectivityManagerApi23Test.class.getSimpleName(); + private static final int SEND_BROADCAST_TIMEOUT = 30000; + // Intent string to get the number of wifi CONNECTIVITY_ACTION callbacks the test app has seen + public static final String GET_WIFI_CONNECTIVITY_ACTION_COUNT = + "android.net.cts.appForApi23.getWifiConnectivityActionCount"; + // Action sent to ConnectivityActionReceiver when a network callback is sent via PendingIntent. + + private Context mContext; + private PackageManager mPackageManager; + private CtsNetUtils mCtsNetUtils; + + @Override + protected void setUp() throws Exception { + super.setUp(); + Looper.prepare(); + mContext = getContext(); + mPackageManager = mContext.getPackageManager(); + mCtsNetUtils = new CtsNetUtils(mContext); + } + + /** + * Tests reporting of connectivity changed. + */ + public void testConnectivityChanged_manifestRequestOnly_shouldNotReceiveIntent() { + if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) { + Log.i(TAG, "testConnectivityChanged_manifestRequestOnly_shouldNotReceiveIntent cannot execute unless device supports WiFi"); + return; + } + ConnectivityReceiver.prepare(); + + mCtsNetUtils.toggleWifi(); + + // The connectivity broadcast has been sent; push through a terminal broadcast + // to wait for in the receive to confirm it didn't see the connectivity change. + Intent finalIntent = new Intent(ConnectivityReceiver.FINAL_ACTION); + finalIntent.setClass(mContext, ConnectivityReceiver.class); + mContext.sendBroadcast(finalIntent); + assertFalse(ConnectivityReceiver.waitForBroadcast()); + } + + public void testConnectivityChanged_manifestRequestOnlyPreN_shouldReceiveIntent() + throws InterruptedException { + if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) { + Log.i(TAG, "testConnectivityChanged_manifestRequestOnlyPreN_shouldReceiveIntent cannot" + + "execute unless device supports WiFi"); + return; + } + mContext.startActivity(new Intent() + .setComponent(new ComponentName("android.net.cts.appForApi23", + "android.net.cts.appForApi23.ConnectivityListeningActivity")) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + Thread.sleep(200); + + mCtsNetUtils.toggleWifi(); + + Intent getConnectivityCount = new Intent(GET_WIFI_CONNECTIVITY_ACTION_COUNT); + assertEquals(2, sendOrderedBroadcastAndReturnResultCode( + getConnectivityCount, SEND_BROADCAST_TIMEOUT)); + } + + public void testConnectivityChanged_whenRegistered_shouldReceiveIntent() { + if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) { + Log.i(TAG, "testConnectivityChanged_whenRegistered_shouldReceiveIntent cannot execute unless device supports WiFi"); + return; + } + ConnectivityReceiver.prepare(); + ConnectivityReceiver receiver = new ConnectivityReceiver(); + IntentFilter filter = new IntentFilter(); + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + mContext.registerReceiver(receiver, filter); + + mCtsNetUtils.toggleWifi(); + Intent finalIntent = new Intent(ConnectivityReceiver.FINAL_ACTION); + finalIntent.setClass(mContext, ConnectivityReceiver.class); + mContext.sendBroadcast(finalIntent); + + assertTrue(ConnectivityReceiver.waitForBroadcast()); + } + + private int sendOrderedBroadcastAndReturnResultCode( + Intent intent, int timeoutMs) throws InterruptedException { + final LinkedBlockingQueue result = new LinkedBlockingQueue<>(1); + mContext.sendOrderedBroadcast(intent, null, new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + result.offer(getResultCode()); + } + }, null, 0, null, null); + + Integer resultCode = result.poll(timeoutMs, TimeUnit.MILLISECONDS); + assertNotNull("Timed out (more than " + timeoutMs + + " milliseconds) waiting for result code for broadcast", resultCode); + return resultCode; + } + +} \ No newline at end of file diff --git a/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityReceiver.java b/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityReceiver.java new file mode 100644 index 0000000000..9d2b8ad2f6 --- /dev/null +++ b/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityReceiver.java @@ -0,0 +1,69 @@ +/* + * 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.cts.api23test; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.util.Log; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class ConnectivityReceiver extends BroadcastReceiver { + static boolean sReceivedConnectivity; + static boolean sReceivedFinal; + static CountDownLatch sLatch; + + static void prepare() { + synchronized (ConnectivityReceiver.class) { + sReceivedConnectivity = sReceivedFinal = false; + sLatch = new CountDownLatch(1); + } + } + + static boolean waitForBroadcast() { + try { + sLatch.await(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + synchronized (ConnectivityReceiver.class) { + sLatch = null; + if (!sReceivedFinal) { + throw new IllegalStateException("Never received final broadcast"); + } + return sReceivedConnectivity; + } + } + + static final String FINAL_ACTION = "android.net.cts.action.FINAL"; + + @Override + public void onReceive(Context context, Intent intent) { + Log.i("ConnectivityReceiver", "Received: " + intent.getAction()); + if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) { + sReceivedConnectivity = true; + } else if (FINAL_ACTION.equals(intent.getAction())) { + sReceivedFinal = true; + if (sLatch != null) { + sLatch.countDown(); + } + } + } +} diff --git a/tests/cts/net/appForApi23/Android.bp b/tests/cts/net/appForApi23/Android.bp new file mode 100644 index 0000000000..cec6d7f5a1 --- /dev/null +++ b/tests/cts/net/appForApi23/Android.bp @@ -0,0 +1,32 @@ +// 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. + +android_test { + name: "CtsNetTestAppForApi23", + defaults: ["cts_defaults"], + + // Include both the 32 and 64 bit versions + compile_multilib: "both", + + srcs: ["src/**/*.java"], + + sdk_version: "23", + + // Tag this module as a cts test artifact + test_suites: [ + "cts", + "general-tests", + ], + +} diff --git a/tests/cts/net/appForApi23/AndroidManifest.xml b/tests/cts/net/appForApi23/AndroidManifest.xml new file mode 100644 index 0000000000..ed4cedbc1d --- /dev/null +++ b/tests/cts/net/appForApi23/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/cts/net/appForApi23/src/android/net/cts/appForApi23/ConnectivityListeningActivity.java b/tests/cts/net/appForApi23/src/android/net/cts/appForApi23/ConnectivityListeningActivity.java new file mode 100644 index 0000000000..24fb68e8cd --- /dev/null +++ b/tests/cts/net/appForApi23/src/android/net/cts/appForApi23/ConnectivityListeningActivity.java @@ -0,0 +1,22 @@ +/* + * 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.cts.appForApi23; + +import android.app.Activity; + +// Stub activity used to start the app +public class ConnectivityListeningActivity extends Activity { +} \ No newline at end of file diff --git a/tests/cts/net/appForApi23/src/android/net/cts/appForApi23/ConnectivityReceiver.java b/tests/cts/net/appForApi23/src/android/net/cts/appForApi23/ConnectivityReceiver.java new file mode 100644 index 0000000000..8039a4f943 --- /dev/null +++ b/tests/cts/net/appForApi23/src/android/net/cts/appForApi23/ConnectivityReceiver.java @@ -0,0 +1,41 @@ +/* + * 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.cts.appForApi23; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; + +public class ConnectivityReceiver extends BroadcastReceiver { + public static String GET_WIFI_CONNECTIVITY_ACTION_COUNT = + "android.net.cts.appForApi23.getWifiConnectivityActionCount"; + + private static int sWifiConnectivityActionCount = 0; + + @Override + public void onReceive(Context context, Intent intent) { + if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) { + int networkType = intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, 0); + if (networkType == ConnectivityManager.TYPE_WIFI) { + sWifiConnectivityActionCount++; + } + } + if (GET_WIFI_CONNECTIVITY_ACTION_COUNT.equals(intent.getAction())) { + setResultCode(sWifiConnectivityActionCount); + } + } +} diff --git a/tests/cts/net/assets/network_watchlist_config_empty_for_test.xml b/tests/cts/net/assets/network_watchlist_config_empty_for_test.xml new file mode 100644 index 0000000000..19628d14ed --- /dev/null +++ b/tests/cts/net/assets/network_watchlist_config_empty_for_test.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/tests/cts/net/assets/network_watchlist_config_for_test.xml b/tests/cts/net/assets/network_watchlist_config_for_test.xml new file mode 100644 index 0000000000..835ae0fea2 --- /dev/null +++ b/tests/cts/net/assets/network_watchlist_config_for_test.xml @@ -0,0 +1,34 @@ + + + + + + F0905DA7549614957B449034C281EF7BDEFDBC2B6E050AD1E78D6DE18FBD0D5F + + + 18DD41C9F2E8E4879A1575FB780514EF33CF6E1F66578C4AE7CCA31F49B9F2EC + + + AAAAAAAA + + + BBBBBBBB + + diff --git a/tests/cts/net/jarjar-rules-shared.txt b/tests/cts/net/jarjar-rules-shared.txt new file mode 100644 index 0000000000..11dba74096 --- /dev/null +++ b/tests/cts/net/jarjar-rules-shared.txt @@ -0,0 +1,2 @@ +# Module library in frameworks/libs/net +rule com.android.net.module.util.** android.net.cts.util.@1 \ No newline at end of file diff --git a/tests/cts/net/jni/Android.bp b/tests/cts/net/jni/Android.bp new file mode 100644 index 0000000000..3953aeb701 --- /dev/null +++ b/tests/cts/net/jni/Android.bp @@ -0,0 +1,51 @@ +// 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. + +cc_library_shared { + name: "libnativedns_jni", + + srcs: ["NativeDnsJni.c"], + sdk_version: "current", + + shared_libs: [ + "libnativehelper_compat_libc++", + "liblog", + ], + stl: "libc++_static", + + cflags: [ + "-Wall", + "-Werror", + "-Wno-unused-parameter", + ], + +} + +cc_library_shared { + name: "libnativemultinetwork_jni", + + srcs: ["NativeMultinetworkJni.cpp"], + sdk_version: "current", + cflags: [ + "-Wall", + "-Werror", + "-Wno-format", + ], + shared_libs: [ + "libandroid", + "libnativehelper_compat_libc++", + "liblog", + ], + stl: "libc++_static", +} diff --git a/tests/cts/net/jni/NativeDnsJni.c b/tests/cts/net/jni/NativeDnsJni.c new file mode 100644 index 0000000000..4ec800e555 --- /dev/null +++ b/tests/cts/net/jni/NativeDnsJni.c @@ -0,0 +1,181 @@ +/* + * 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. + */ + +#include +#include +#include +#include +#include + +#include + +#define LOG_TAG "NativeDns-JNI" +#define LOGD(fmt, ...) \ + __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, fmt, ##__VA_ARGS__) + +const char *GoogleDNSIpV4Address="8.8.8.8"; +const char *GoogleDNSIpV4Address2="8.8.4.4"; +const char *GoogleDNSIpV6Address="2001:4860:4860::8888"; +const char *GoogleDNSIpV6Address2="2001:4860:4860::8844"; + +JNIEXPORT jboolean Java_android_net_cts_DnsTest_testNativeDns(JNIEnv* env, jclass class) +{ + const char *node = "www.google.com"; + char *service = NULL; + struct addrinfo *answer; + + int res = getaddrinfo(node, service, NULL, &answer); + LOGD("getaddrinfo(www.google.com) gave res=%d (%s)", res, gai_strerror(res)); + if (res != 0) return JNI_FALSE; + + // check for v4 & v6 + { + int foundv4 = 0; + int foundv6 = 0; + struct addrinfo *current = answer; + while (current != NULL) { + char buf[256]; + if (current->ai_addr->sa_family == AF_INET) { + inet_ntop(current->ai_family, &((struct sockaddr_in *)current->ai_addr)->sin_addr, + buf, sizeof(buf)); + foundv4 = 1; + LOGD(" %s", buf); + } else if (current->ai_addr->sa_family == AF_INET6) { + inet_ntop(current->ai_family, &((struct sockaddr_in6 *)current->ai_addr)->sin6_addr, + buf, sizeof(buf)); + foundv6 = 1; + LOGD(" %s", buf); + } + current = current->ai_next; + } + + freeaddrinfo(answer); + answer = NULL; + if (foundv4 != 1 && foundv6 != 1) { + LOGD("getaddrinfo(www.google.com) didn't find either v4 or v6 address"); + return JNI_FALSE; + } + } + + node = "ipv6.google.com"; + res = getaddrinfo(node, service, NULL, &answer); + LOGD("getaddrinfo(ipv6.google.com) gave res=%d", res); + if (res != 0) return JNI_FALSE; + + { + int foundv4 = 0; + int foundv6 = 0; + struct addrinfo *current = answer; + while (current != NULL) { + char buf[256]; + if (current->ai_addr->sa_family == AF_INET) { + inet_ntop(current->ai_family, &((struct sockaddr_in *)current->ai_addr)->sin_addr, + buf, sizeof(buf)); + LOGD(" %s", buf); + foundv4 = 1; + } else if (current->ai_addr->sa_family == AF_INET6) { + inet_ntop(current->ai_family, &((struct sockaddr_in6 *)current->ai_addr)->sin6_addr, + buf, sizeof(buf)); + LOGD(" %s", buf); + foundv6 = 1; + } + current = current->ai_next; + } + + freeaddrinfo(answer); + answer = NULL; + if (foundv4 == 1 || foundv6 != 1) { + LOGD("getaddrinfo(ipv6.google.com) didn't find only v6"); + return JNI_FALSE; + } + } + + // getnameinfo + struct sockaddr_in sa4; + sa4.sin_family = AF_INET; + sa4.sin_port = 0; + inet_pton(AF_INET, GoogleDNSIpV4Address, &(sa4.sin_addr)); + + struct sockaddr_in6 sa6; + sa6.sin6_family = AF_INET6; + sa6.sin6_port = 0; + sa6.sin6_flowinfo = 0; + sa6.sin6_scope_id = 0; + inet_pton(AF_INET6, GoogleDNSIpV6Address2, &(sa6.sin6_addr)); + + char buf[NI_MAXHOST]; + int flags = NI_NAMEREQD; + + res = getnameinfo((const struct sockaddr*)&sa4, sizeof(sa4), buf, sizeof(buf), NULL, 0, flags); + if (res != 0) { + LOGD("getnameinfo(%s (GoogleDNS) ) gave error %d (%s)", GoogleDNSIpV4Address, res, + gai_strerror(res)); + return JNI_FALSE; + } + if (strstr(buf, "google.com") == NULL && strstr(buf, "dns.google") == NULL) { + LOGD("getnameinfo(%s (GoogleDNS) ) didn't return google.com or dns.google: %s", + GoogleDNSIpV4Address, buf); + return JNI_FALSE; + } + + memset(buf, 0, sizeof(buf)); + res = getnameinfo((const struct sockaddr*)&sa6, sizeof(sa6), buf, sizeof(buf), NULL, 0, flags); + if (res != 0) { + LOGD("getnameinfo(%s (GoogleDNS) ) gave error %d (%s)", GoogleDNSIpV6Address2, + res, gai_strerror(res)); + return JNI_FALSE; + } + if (strstr(buf, "google.com") == NULL && strstr(buf, "dns.google") == NULL) { + LOGD("getnameinfo(%s (GoogleDNS) ) didn't return google.com or dns.google: %s", + GoogleDNSIpV6Address2, buf); + return JNI_FALSE; + } + + // gethostbyname + struct hostent *my_hostent = gethostbyname("www.youtube.com"); + if (my_hostent == NULL) { + LOGD("gethostbyname(www.youtube.com) gave null response"); + return JNI_FALSE; + } + if ((my_hostent->h_addr_list == NULL) || (*my_hostent->h_addr_list == NULL)) { + LOGD("gethostbyname(www.youtube.com) gave 0 addresses"); + return JNI_FALSE; + } + { + char **current = my_hostent->h_addr_list; + while (*current != NULL) { + char buf[256]; + inet_ntop(my_hostent->h_addrtype, *current, buf, sizeof(buf)); + LOGD("gethostbyname(www.youtube.com) gave %s", buf); + current++; + } + } + + // gethostbyaddr + char addr6[16]; + inet_pton(AF_INET6, GoogleDNSIpV6Address, addr6); + my_hostent = gethostbyaddr(addr6, sizeof(addr6), AF_INET6); + if (my_hostent == NULL) { + LOGD("gethostbyaddr(%s (GoogleDNS) ) gave null response", GoogleDNSIpV6Address); + return JNI_FALSE; + } + + LOGD("gethostbyaddr(%s (GoogleDNS) ) gave %s for name", GoogleDNSIpV6Address, + my_hostent->h_name ? my_hostent->h_name : "null"); + + if (my_hostent->h_name == NULL) return JNI_FALSE; + return JNI_TRUE; +} diff --git a/tests/cts/net/jni/NativeMultinetworkJni.cpp b/tests/cts/net/jni/NativeMultinetworkJni.cpp new file mode 100644 index 0000000000..60e31bc78a --- /dev/null +++ b/tests/cts/net/jni/NativeMultinetworkJni.cpp @@ -0,0 +1,515 @@ +/* + * 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. + */ + + +#define LOG_TAG "MultinetworkApiTest" + +#include +#include +#include +#include +#include +#include +#include /* poll */ +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#define LOGD(fmt, ...) \ + __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, fmt, ##__VA_ARGS__) + +#define EXPECT_GE(env, actual, expected, msg) \ + do { \ + if (actual < expected) { \ + jniThrowExceptionFmt(env, "java/lang/AssertionError", \ + "%s:%d: %s EXPECT_GE: expected %d, got %d", \ + __FILE__, __LINE__, msg, expected, actual); \ + } \ + } while (0) + +#define EXPECT_GT(env, actual, expected, msg) \ + do { \ + if (actual <= expected) { \ + jniThrowExceptionFmt(env, "java/lang/AssertionError", \ + "%s:%d: %s EXPECT_GT: expected %d, got %d", \ + __FILE__, __LINE__, msg, expected, actual); \ + } \ + } while (0) + +#define EXPECT_EQ(env, expected, actual, msg) \ + do { \ + if (actual != expected) { \ + jniThrowExceptionFmt(env, "java/lang/AssertionError", \ + "%s:%d: %s EXPECT_EQ: expected %d, got %d", \ + __FILE__, __LINE__, msg, expected, actual); \ + } \ + } while (0) + +static const int MAXPACKET = 8 * 1024; +static const int TIMEOUT_MS = 15000; +static const char kHostname[] = "connectivitycheck.android.com"; +static const char kNxDomainName[] = "test1-nx.metric.gstatic.com"; +static const char kGoogleName[] = "www.google.com"; + +int makeQuery(const char* name, int qtype, uint8_t* buf, size_t buflen) { + return res_mkquery(ns_o_query, name, ns_c_in, qtype, NULL, 0, NULL, buf, buflen); +} + +int getAsyncResponse(JNIEnv* env, int fd, int timeoutMs, int* rcode, uint8_t* buf, size_t bufLen) { + struct pollfd wait_fd = { .fd = fd, .events = POLLIN }; + + poll(&wait_fd, 1, timeoutMs); + if (wait_fd.revents & POLLIN) { + int n = android_res_nresult(fd, rcode, buf, bufLen); + // Verify that android_res_nresult() closed the fd + char dummy; + EXPECT_EQ(env, -1, read(fd, &dummy, sizeof(dummy)), "res_nresult check for closing fd"); + EXPECT_EQ(env, EBADF, errno, "res_nresult check for errno"); + return n; + } + + return -ETIMEDOUT; +} + +int extractIpAddressAnswers(uint8_t* buf, size_t bufLen, int family) { + ns_msg handle; + if (ns_initparse((const uint8_t*) buf, bufLen, &handle) < 0) { + return -errno; + } + const int ancount = ns_msg_count(handle, ns_s_an); + // Answer count = 0 is valid(e.g. response of query with root) + if (!ancount) { + return 0; + } + ns_rr rr; + bool hasValidAns = false; + for (int i = 0; i < ancount; i++) { + if (ns_parserr(&handle, ns_s_an, i, &rr) < 0) { + // If there is no valid answer, test will fail. + continue; + } + const uint8_t* rdata = ns_rr_rdata(rr); + char buffer[INET6_ADDRSTRLEN]; + if (inet_ntop(family, (const char*) rdata, buffer, sizeof(buffer)) == NULL) { + return -errno; + } + hasValidAns = true; + } + return hasValidAns ? 0 : -EBADMSG; +} + +int expectAnswersValid(JNIEnv* env, int fd, int family, int expectedRcode) { + int rcode = -1; + uint8_t buf[MAXPACKET] = {}; + int res = getAsyncResponse(env, fd, TIMEOUT_MS, &rcode, buf, MAXPACKET); + if (res < 0) { + return res; + } + + EXPECT_EQ(env, expectedRcode, rcode, "rcode is not expected"); + + if (expectedRcode == ns_r_noerror && res > 0) { + return extractIpAddressAnswers(buf, res, family); + } + return 0; +} + +int expectAnswersNotValid(JNIEnv* env, int fd, int expectedErrno) { + int rcode = -1; + uint8_t buf[MAXPACKET] = {}; + int res = getAsyncResponse(env, fd, TIMEOUT_MS, &rcode, buf, MAXPACKET); + if (res != expectedErrno) { + LOGD("res:%d, expectedErrno = %d", res, expectedErrno); + return (res > 0) ? -EREMOTEIO : res; + } + return 0; +} + +extern "C" +JNIEXPORT void Java_android_net_cts_MultinetworkApiTest_runResNqueryCheck( + JNIEnv* env, jclass, jlong nethandle) { + net_handle_t handle = (net_handle_t) nethandle; + + // V4 + int fd = android_res_nquery(handle, kHostname, ns_c_in, ns_t_a, 0); + EXPECT_GE(env, fd, 0, "v4 res_nquery"); + EXPECT_EQ(env, 0, expectAnswersValid(env, fd, AF_INET, ns_r_noerror), + "v4 res_nquery check answers"); + + // V6 + fd = android_res_nquery(handle, kHostname, ns_c_in, ns_t_aaaa, 0); + EXPECT_GE(env, fd, 0, "v6 res_nquery"); + EXPECT_EQ(env, 0, expectAnswersValid(env, fd, AF_INET, ns_r_noerror), + "v6 res_nquery check answers"); +} + +extern "C" +JNIEXPORT void Java_android_net_cts_MultinetworkApiTest_runResNsendCheck( + JNIEnv* env, jclass, jlong nethandle) { + net_handle_t handle = (net_handle_t) nethandle; + // V4 + uint8_t buf1[MAXPACKET] = {}; + + int len1 = makeQuery(kGoogleName, ns_t_a, buf1, sizeof(buf1)); + EXPECT_GT(env, len1, 0, "v4 res_mkquery 1st"); + + uint8_t buf2[MAXPACKET] = {}; + int len2 = makeQuery(kHostname, ns_t_a, buf2, sizeof(buf2)); + EXPECT_GT(env, len2, 0, "v4 res_mkquery 2nd"); + + int fd1 = android_res_nsend(handle, buf1, len1, 0); + EXPECT_GE(env, fd1, 0, "v4 res_nsend 1st"); + int fd2 = android_res_nsend(handle, buf2, len2, 0); + EXPECT_GE(env, fd2, 0, "v4 res_nsend 2nd"); + + EXPECT_EQ(env, 0, expectAnswersValid(env, fd2, AF_INET, ns_r_noerror), + "v4 res_nsend 2nd check answers"); + EXPECT_EQ(env, 0, expectAnswersValid(env, fd1, AF_INET, ns_r_noerror), + "v4 res_nsend 1st check answers"); + + // V6 + memset(buf1, 0, sizeof(buf1)); + memset(buf2, 0, sizeof(buf2)); + len1 = makeQuery(kGoogleName, ns_t_aaaa, buf1, sizeof(buf1)); + EXPECT_GT(env, len1, 0, "v6 res_mkquery 1st"); + len2 = makeQuery(kHostname, ns_t_aaaa, buf2, sizeof(buf2)); + EXPECT_GT(env, len2, 0, "v6 res_mkquery 2nd"); + + fd1 = android_res_nsend(handle, buf1, len1, 0); + EXPECT_GE(env, fd1, 0, "v6 res_nsend 1st"); + fd2 = android_res_nsend(handle, buf2, len2, 0); + EXPECT_GE(env, fd2, 0, "v6 res_nsend 2nd"); + + EXPECT_EQ(env, 0, expectAnswersValid(env, fd2, AF_INET6, ns_r_noerror), + "v6 res_nsend 2nd check answers"); + EXPECT_EQ(env, 0, expectAnswersValid(env, fd1, AF_INET6, ns_r_noerror), + "v6 res_nsend 1st check answers"); +} + +extern "C" +JNIEXPORT void Java_android_net_cts_MultinetworkApiTest_runResNnxDomainCheck( + JNIEnv* env, jclass, jlong nethandle) { + net_handle_t handle = (net_handle_t) nethandle; + + // res_nquery V4 NXDOMAIN + int fd = android_res_nquery(handle, kNxDomainName, ns_c_in, ns_t_a, 0); + EXPECT_GE(env, fd, 0, "v4 res_nquery NXDOMAIN"); + EXPECT_EQ(env, 0, expectAnswersValid(env, fd, AF_INET, ns_r_nxdomain), + "v4 res_nquery NXDOMAIN check answers"); + + // res_nquery V6 NXDOMAIN + fd = android_res_nquery(handle, kNxDomainName, ns_c_in, ns_t_aaaa, 0); + EXPECT_GE(env, fd, 0, "v6 res_nquery NXDOMAIN"); + EXPECT_EQ(env, 0, expectAnswersValid(env, fd, AF_INET6, ns_r_nxdomain), + "v6 res_nquery NXDOMAIN check answers"); + + uint8_t buf[MAXPACKET] = {}; + // res_nsend V4 NXDOMAIN + int len = makeQuery(kNxDomainName, ns_t_a, buf, sizeof(buf)); + EXPECT_GT(env, len, 0, "v4 res_mkquery NXDOMAIN"); + fd = android_res_nsend(handle, buf, len, 0); + EXPECT_GE(env, fd, 0, "v4 res_nsend NXDOMAIN"); + EXPECT_EQ(env, 0, expectAnswersValid(env, fd, AF_INET, ns_r_nxdomain), + "v4 res_nsend NXDOMAIN check answers"); + + // res_nsend V6 NXDOMAIN + memset(buf, 0, sizeof(buf)); + len = makeQuery(kNxDomainName, ns_t_aaaa, buf, sizeof(buf)); + EXPECT_GT(env, len, 0, "v6 res_mkquery NXDOMAIN"); + fd = android_res_nsend(handle, buf, len, 0); + EXPECT_GE(env, fd, 0, "v6 res_nsend NXDOMAIN"); + EXPECT_EQ(env, 0, expectAnswersValid(env, fd, AF_INET6, ns_r_nxdomain), + "v6 res_nsend NXDOMAIN check answers"); +} + + +extern "C" +JNIEXPORT void Java_android_net_cts_MultinetworkApiTest_runResNcancelCheck( + JNIEnv* env, jclass, jlong nethandle) { + net_handle_t handle = (net_handle_t) nethandle; + + int fd = android_res_nquery(handle, kGoogleName, ns_c_in, ns_t_a, 0); + errno = 0; + android_res_cancel(fd); + int err = errno; + EXPECT_EQ(env, 0, err, "res_cancel"); + // DO NOT call cancel or result with the same fd more than once, + // otherwise it will hit fdsan double-close fd. +} + +extern "C" +JNIEXPORT void Java_android_net_cts_MultinetworkApiTest_runResNapiMalformedCheck( + JNIEnv* env, jclass, jlong nethandle) { + net_handle_t handle = (net_handle_t) nethandle; + + // It is the equivalent of "dig . a", Query with an empty name. + int fd = android_res_nquery(handle, "", ns_c_in, ns_t_a, 0); + EXPECT_GE(env, fd, 0, "res_nquery root"); + EXPECT_EQ(env, 0, expectAnswersValid(env, fd, AF_INET, ns_r_noerror), + "res_nquery root check answers"); + + // Label limit 63 + std::string exceedingLabelQuery = "www." + std::string(70, 'g') + ".com"; + // Name limit 255 + std::string exceedingDomainQuery = "www." + std::string(255, 'g') + ".com"; + + fd = android_res_nquery(handle, exceedingLabelQuery.c_str(), ns_c_in, ns_t_a, 0); + EXPECT_EQ(env, -EMSGSIZE, fd, "res_nquery exceedingLabelQuery"); + fd = android_res_nquery(handle, exceedingDomainQuery.c_str(), ns_c_in, ns_t_aaaa, 0); + EXPECT_EQ(env, -EMSGSIZE, fd, "res_nquery exceedingDomainQuery"); + + uint8_t buf[10] = {}; + // empty BLOB + fd = android_res_nsend(handle, buf, 10, 0); + EXPECT_GE(env, fd, 0, "res_nsend empty BLOB"); + EXPECT_EQ(env, 0, expectAnswersNotValid(env, fd, -EINVAL), + "res_nsend empty BLOB check answers"); + + uint8_t largeBuf[2 * MAXPACKET] = {}; + // A buffer larger than 8KB + fd = android_res_nsend(handle, largeBuf, sizeof(largeBuf), 0); + EXPECT_EQ(env, -EMSGSIZE, fd, "res_nsend buffer larger than 8KB"); + + // 5000 bytes filled with 0. This returns EMSGSIZE because FrameworkListener limits the size of + // commands to 4096 bytes. + fd = android_res_nsend(handle, largeBuf, 5000, 0); + EXPECT_EQ(env, -EMSGSIZE, fd, "res_nsend 5000 bytes filled with 0"); + + // 500 bytes filled with 0 + fd = android_res_nsend(handle, largeBuf, 500, 0); + EXPECT_GE(env, fd, 0, "res_nsend 500 bytes filled with 0"); + EXPECT_EQ(env, 0, expectAnswersNotValid(env, fd, -EINVAL), + "res_nsend 500 bytes filled with 0 check answers"); + + // 5000 bytes filled with 0xFF + uint8_t ffBuf[5001] = {}; + memset(ffBuf, 0xFF, sizeof(ffBuf)); + ffBuf[5000] = '\0'; + fd = android_res_nsend(handle, ffBuf, sizeof(ffBuf), 0); + EXPECT_EQ(env, -EMSGSIZE, fd, "res_nsend 5000 bytes filled with 0xFF"); + + // 500 bytes filled with 0xFF + ffBuf[500] = '\0'; + fd = android_res_nsend(handle, ffBuf, 501, 0); + EXPECT_GE(env, fd, 0, "res_nsend 500 bytes filled with 0xFF"); + EXPECT_EQ(env, 0, expectAnswersNotValid(env, fd, -EINVAL), + "res_nsend 500 bytes filled with 0xFF check answers"); +} + +extern "C" +JNIEXPORT jint Java_android_net_cts_MultinetworkApiTest_runGetaddrinfoCheck( + JNIEnv*, jclass, jlong nethandle) { + net_handle_t handle = (net_handle_t) nethandle; + struct addrinfo *res = NULL; + + errno = 0; + int rval = android_getaddrinfofornetwork(handle, kHostname, NULL, NULL, &res); + const int saved_errno = errno; + freeaddrinfo(res); + + LOGD("android_getaddrinfofornetwork(%" PRIu64 ", %s) returned rval=%d errno=%d", + handle, kHostname, rval, saved_errno); + return rval == 0 ? 0 : -saved_errno; +} + +extern "C" +JNIEXPORT jint Java_android_net_cts_MultinetworkApiTest_runSetprocnetwork( + JNIEnv*, jclass, jlong nethandle) { + net_handle_t handle = (net_handle_t) nethandle; + + errno = 0; + int rval = android_setprocnetwork(handle); + const int saved_errno = errno; + LOGD("android_setprocnetwork(%" PRIu64 ") returned rval=%d errno=%d", + handle, rval, saved_errno); + return rval == 0 ? 0 : -saved_errno; +} + +extern "C" +JNIEXPORT jint Java_android_net_cts_MultinetworkApiTest_runSetsocknetwork( + JNIEnv*, jclass, jlong nethandle) { + net_handle_t handle = (net_handle_t) nethandle; + + errno = 0; + int fd = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP); + if (fd < 0) { + LOGD("socket() failed, errno=%d", errno); + return -errno; + } + + errno = 0; + int rval = android_setsocknetwork(handle, fd); + const int saved_errno = errno; + LOGD("android_setprocnetwork(%" PRIu64 ", %d) returned rval=%d errno=%d", + handle, fd, rval, saved_errno); + close(fd); + return rval == 0 ? 0 : -saved_errno; +} + +// Use sizeof("x") - 1 because we need a compile-time constant, and strlen("x") +// isn't guaranteed to fold to a constant. +static const int kSockaddrStrLen = INET6_ADDRSTRLEN + sizeof("[]:65535") - 1; + +void sockaddr_ntop(const struct sockaddr *sa, socklen_t salen, char *dst, const size_t size) { + char addrstr[INET6_ADDRSTRLEN]; + char portstr[sizeof("65535")]; + char buf[kSockaddrStrLen+1]; + + int ret = getnameinfo(sa, salen, + addrstr, sizeof(addrstr), + portstr, sizeof(portstr), + NI_NUMERICHOST | NI_NUMERICSERV); + if (ret == 0) { + snprintf(buf, sizeof(buf), + (sa->sa_family == AF_INET6) ? "[%s]:%s" : "%s:%s", + addrstr, portstr); + } else { + sprintf(buf, "???"); + } + + strlcpy(dst, buf, size); +} + +extern "C" +JNIEXPORT jint Java_android_net_cts_MultinetworkApiTest_runDatagramCheck( + JNIEnv*, jclass, jlong nethandle) { + const struct addrinfo kHints = { + .ai_flags = AI_ADDRCONFIG, + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_DGRAM, + .ai_protocol = IPPROTO_UDP, + }; + struct addrinfo *res = NULL; + net_handle_t handle = (net_handle_t) nethandle; + + static const char kPort[] = "443"; + int rval = android_getaddrinfofornetwork(handle, kHostname, kPort, &kHints, &res); + if (rval != 0) { + LOGD("android_getaddrinfofornetwork(%llu, %s) returned rval=%d errno=%d", + handle, kHostname, rval, errno); + freeaddrinfo(res); + return -errno; + } + + // Rely upon getaddrinfo sorting the best destination to the front. + int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if (fd < 0) { + LOGD("socket(%d, %d, %d) failed, errno=%d", + res->ai_family, res->ai_socktype, res->ai_protocol, errno); + freeaddrinfo(res); + return -errno; + } + + rval = android_setsocknetwork(handle, fd); + LOGD("android_setprocnetwork(%llu, %d) returned rval=%d errno=%d", + handle, fd, rval, errno); + if (rval != 0) { + close(fd); + freeaddrinfo(res); + return -errno; + } + + char addrstr[kSockaddrStrLen+1]; + sockaddr_ntop(res->ai_addr, res->ai_addrlen, addrstr, sizeof(addrstr)); + LOGD("Attempting connect() to %s ...", addrstr); + + rval = connect(fd, res->ai_addr, res->ai_addrlen); + if (rval != 0) { + close(fd); + freeaddrinfo(res); + return -errno; + } + freeaddrinfo(res); + + struct sockaddr_storage src_addr; + socklen_t src_addrlen = sizeof(src_addr); + if (getsockname(fd, (struct sockaddr *)&src_addr, &src_addrlen) != 0) { + close(fd); + return -errno; + } + sockaddr_ntop((const struct sockaddr *)&src_addr, sizeof(src_addr), addrstr, sizeof(addrstr)); + LOGD("... from %s", addrstr); + + // Don't let reads or writes block indefinitely. + const struct timeval timeo = { 2, 0 }; // 2 seconds + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeo, sizeof(timeo)); + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeo, sizeof(timeo)); + + // For reference see: + // https://datatracker.ietf.org/doc/html/draft-ietf-quic-invariants + uint8_t quic_packet[1200] = { + 0xc0, // long header + 0xaa, 0xda, 0xca, 0xca, // reserved-space version number + 0x08, // destination connection ID length + 0, 0, 0, 0, 0, 0, 0, 0, // 64bit connection ID + 0x00, // source connection ID length + }; + + arc4random_buf(quic_packet + 6, 8); // random connection ID + + uint8_t response[1500]; + ssize_t sent, rcvd; + static const int MAX_RETRIES = 5; + int i, errnum = 0; + + for (i = 0; i < MAX_RETRIES; i++) { + sent = send(fd, quic_packet, sizeof(quic_packet), 0); + if (sent < (ssize_t)sizeof(quic_packet)) { + errnum = errno; + LOGD("send(QUIC packet) returned sent=%zd, errno=%d", sent, errnum); + close(fd); + return -errnum; + } + + rcvd = recv(fd, response, sizeof(response), 0); + if (rcvd > 0) { + break; + } else { + errnum = errno; + LOGD("[%d/%d] recv(QUIC response) returned rcvd=%zd, errno=%d", + i + 1, MAX_RETRIES, rcvd, errnum); + } + } + if (rcvd < 15) { + LOGD("QUIC UDP %s: sent=%zd but rcvd=%zd, errno=%d", kPort, sent, rcvd, errnum); + if (rcvd <= 0) { + LOGD("Does this network block UDP port %s?", kPort); + } + close(fd); + return -EPROTO; + } + + int conn_id_cmp = memcmp(quic_packet + 6, response + 7, 8); + if (conn_id_cmp != 0) { + LOGD("sent and received connection IDs do not match"); + close(fd); + return -EPROTO; + } + + // TODO: Replace this quick 'n' dirty test with proper QUIC-capable code. + + close(fd); + return 0; +} diff --git a/tests/cts/net/native/dns/Android.bp b/tests/cts/net/native/dns/Android.bp new file mode 100644 index 0000000000..6defd359ab --- /dev/null +++ b/tests/cts/net/native/dns/Android.bp @@ -0,0 +1,41 @@ +cc_defaults { + name: "dns_async_defaults", + + cflags: [ + "-fstack-protector-all", + "-g", + "-Wall", + "-Wextra", + "-Werror", + "-Wnullable-to-nonnull-conversion", + "-Wsign-compare", + "-Wthread-safety", + "-Wunused-parameter", + ], + srcs: [ + "NativeDnsAsyncTest.cpp", + ], + shared_libs: [ + "libandroid", + "liblog", + "libutils", + ], +} + +cc_test { + name: "CtsNativeNetDnsTestCases", + defaults: ["dns_async_defaults"], + multilib: { + lib32: { + suffix: "32", + }, + lib64: { + suffix: "64", + }, + }, + test_suites: [ + "cts", + "general-tests", + "mts", + ], +} diff --git a/tests/cts/net/native/dns/AndroidTest.xml b/tests/cts/net/native/dns/AndroidTest.xml new file mode 100644 index 0000000000..6d03c23448 --- /dev/null +++ b/tests/cts/net/native/dns/AndroidTest.xml @@ -0,0 +1,32 @@ + + + + diff --git a/tests/cts/net/native/dns/NativeDnsAsyncTest.cpp b/tests/cts/net/native/dns/NativeDnsAsyncTest.cpp new file mode 100644 index 0000000000..e501475996 --- /dev/null +++ b/tests/cts/net/native/dns/NativeDnsAsyncTest.cpp @@ -0,0 +1,257 @@ +/* + * 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include /* poll */ +#include +#include +#include + +#include +#include + +namespace { +constexpr int MAXPACKET = 8 * 1024; +constexpr int PTON_MAX = 16; +constexpr int TIMEOUT_MS = 10000; + +int getAsyncResponse(int fd, int timeoutMs, int* rcode, uint8_t* buf, size_t bufLen) { + struct pollfd wait_fd[1]; + wait_fd[0].fd = fd; + wait_fd[0].events = POLLIN; + short revents; + int ret; + ret = poll(wait_fd, 1, timeoutMs); + revents = wait_fd[0].revents; + if (revents & POLLIN) { + int n = android_res_nresult(fd, rcode, buf, bufLen); + // Verify that android_res_nresult() closed the fd + char dummy; + EXPECT_EQ(-1, read(fd, &dummy, sizeof dummy)); + EXPECT_EQ(EBADF, errno); + return n; + } + + return -1; +} + +std::vector extractIpAddressAnswers(uint8_t* buf, size_t bufLen, int ipType) { + ns_msg handle; + if (ns_initparse((const uint8_t*) buf, bufLen, &handle) < 0) { + return {}; + } + const int ancount = ns_msg_count(handle, ns_s_an); + ns_rr rr; + std::vector answers; + for (int i = 0; i < ancount; i++) { + if (ns_parserr(&handle, ns_s_an, i, &rr) < 0) { + continue; + } + const uint8_t* rdata = ns_rr_rdata(rr); + char buffer[INET6_ADDRSTRLEN]; + if (inet_ntop(ipType, (const char*) rdata, buffer, sizeof(buffer))) { + answers.push_back(buffer); + } + } + return answers; +} + +void expectAnswersValid(int fd, int ipType, int expectedRcode) { + int rcode = -1; + uint8_t buf[MAXPACKET] = {}; + int res = getAsyncResponse(fd, TIMEOUT_MS, &rcode, buf, MAXPACKET); + EXPECT_GE(res, 0); + EXPECT_EQ(rcode, expectedRcode); + + if (expectedRcode == ns_r_noerror) { + auto answers = extractIpAddressAnswers(buf, res, ipType); + EXPECT_GE(answers.size(), 0U); + for (auto &answer : answers) { + char pton[PTON_MAX]; + EXPECT_EQ(1, inet_pton(ipType, answer.c_str(), pton)); + } + } +} + +void expectAnswersNotValid(int fd, int expectedErrno) { + int rcode = -1; + uint8_t buf[MAXPACKET] = {}; + int res = getAsyncResponse(fd, TIMEOUT_MS, &rcode, buf, MAXPACKET); + EXPECT_EQ(expectedErrno, res); +} + +} // namespace + +TEST (NativeDnsAsyncTest, Async_Query) { + // V4 + int fd1 = android_res_nquery( + NETWORK_UNSPECIFIED, "www.google.com", ns_c_in, ns_t_a, 0); + EXPECT_GE(fd1, 0); + int fd2 = android_res_nquery( + NETWORK_UNSPECIFIED, "www.youtube.com", ns_c_in, ns_t_a, 0); + EXPECT_GE(fd2, 0); + expectAnswersValid(fd2, AF_INET, ns_r_noerror); + expectAnswersValid(fd1, AF_INET, ns_r_noerror); + + // V6 + fd1 = android_res_nquery( + NETWORK_UNSPECIFIED, "www.google.com", ns_c_in, ns_t_aaaa, 0); + EXPECT_GE(fd1, 0); + fd2 = android_res_nquery( + NETWORK_UNSPECIFIED, "www.youtube.com", ns_c_in, ns_t_aaaa, 0); + EXPECT_GE(fd2, 0); + expectAnswersValid(fd2, AF_INET6, ns_r_noerror); + expectAnswersValid(fd1, AF_INET6, ns_r_noerror); +} + +TEST (NativeDnsAsyncTest, Async_Send) { + // V4 + uint8_t buf1[MAXPACKET] = {}; + int len1 = res_mkquery(ns_o_query, "www.googleapis.com", + ns_c_in, ns_t_a, nullptr, 0, nullptr, buf1, sizeof(buf1)); + EXPECT_GT(len1, 0); + + uint8_t buf2[MAXPACKET] = {}; + int len2 = res_mkquery(ns_o_query, "play.googleapis.com", + ns_c_in, ns_t_a, nullptr, 0, nullptr, buf2, sizeof(buf2)); + EXPECT_GT(len2, 0); + + int fd1 = android_res_nsend(NETWORK_UNSPECIFIED, buf1, len1, 0); + EXPECT_GE(fd1, 0); + int fd2 = android_res_nsend(NETWORK_UNSPECIFIED, buf2, len2, 0); + EXPECT_GE(fd2, 0); + + expectAnswersValid(fd2, AF_INET, ns_r_noerror); + expectAnswersValid(fd1, AF_INET, ns_r_noerror); + + // V6 + memset(buf1, 0, sizeof(buf1)); + memset(buf2, 0, sizeof(buf2)); + len1 = res_mkquery(ns_o_query, "www.googleapis.com", + ns_c_in, ns_t_aaaa, nullptr, 0, nullptr, buf1, sizeof(buf1)); + EXPECT_GT(len1, 0); + len2 = res_mkquery(ns_o_query, "play.googleapis.com", + ns_c_in, ns_t_aaaa, nullptr, 0, nullptr, buf2, sizeof(buf2)); + EXPECT_GT(len2, 0); + + fd1 = android_res_nsend(NETWORK_UNSPECIFIED, buf1, len1, 0); + EXPECT_GE(fd1, 0); + fd2 = android_res_nsend(NETWORK_UNSPECIFIED, buf2, len2, 0); + EXPECT_GE(fd2, 0); + + expectAnswersValid(fd2, AF_INET6, ns_r_noerror); + expectAnswersValid(fd1, AF_INET6, ns_r_noerror); +} + +TEST (NativeDnsAsyncTest, Async_NXDOMAIN) { + uint8_t buf[MAXPACKET] = {}; + int len = res_mkquery(ns_o_query, "test1-nx.metric.gstatic.com", + ns_c_in, ns_t_a, nullptr, 0, nullptr, buf, sizeof(buf)); + EXPECT_GT(len, 0); + int fd1 = android_res_nsend(NETWORK_UNSPECIFIED, buf, len, ANDROID_RESOLV_NO_CACHE_LOOKUP); + EXPECT_GE(fd1, 0); + + len = res_mkquery(ns_o_query, "test2-nx.metric.gstatic.com", + ns_c_in, ns_t_a, nullptr, 0, nullptr, buf, sizeof(buf)); + EXPECT_GT(len, 0); + int fd2 = android_res_nsend(NETWORK_UNSPECIFIED, buf, len, ANDROID_RESOLV_NO_CACHE_LOOKUP); + EXPECT_GE(fd2, 0); + + expectAnswersValid(fd2, AF_INET, ns_r_nxdomain); + expectAnswersValid(fd1, AF_INET, ns_r_nxdomain); + + fd1 = android_res_nquery( + NETWORK_UNSPECIFIED, "test3-nx.metric.gstatic.com", + ns_c_in, ns_t_aaaa, ANDROID_RESOLV_NO_CACHE_LOOKUP); + EXPECT_GE(fd1, 0); + fd2 = android_res_nquery( + NETWORK_UNSPECIFIED, "test4-nx.metric.gstatic.com", + ns_c_in, ns_t_aaaa, ANDROID_RESOLV_NO_CACHE_LOOKUP); + EXPECT_GE(fd2, 0); + expectAnswersValid(fd2, AF_INET6, ns_r_nxdomain); + expectAnswersValid(fd1, AF_INET6, ns_r_nxdomain); +} + +TEST (NativeDnsAsyncTest, Async_Cancel) { + int fd = android_res_nquery( + NETWORK_UNSPECIFIED, "www.google.com", ns_c_in, ns_t_a, 0); + errno = 0; + android_res_cancel(fd); + int err = errno; + EXPECT_EQ(err, 0); + // DO NOT call cancel or result with the same fd more than once, + // otherwise it will hit fdsan double-close fd. +} + +TEST (NativeDnsAsyncTest, Async_Query_MALFORMED) { + // Empty string to create BLOB and query, we will get empty result and rcode = 0 + // on DNSTLS. + int fd = android_res_nquery( + NETWORK_UNSPECIFIED, "", ns_c_in, ns_t_a, 0); + EXPECT_GE(fd, 0); + expectAnswersValid(fd, AF_INET, ns_r_noerror); + + std::string exceedingLabelQuery = "www." + std::string(70, 'g') + ".com"; + std::string exceedingDomainQuery = "www." + std::string(255, 'g') + ".com"; + + fd = android_res_nquery(NETWORK_UNSPECIFIED, + exceedingLabelQuery.c_str(), ns_c_in, ns_t_a, 0); + EXPECT_EQ(-EMSGSIZE, fd); + fd = android_res_nquery(NETWORK_UNSPECIFIED, + exceedingDomainQuery.c_str(), ns_c_in, ns_t_a, 0); + EXPECT_EQ(-EMSGSIZE, fd); +} + +TEST (NativeDnsAsyncTest, Async_Send_MALFORMED) { + uint8_t buf[10] = {}; + // empty BLOB + int fd = android_res_nsend(NETWORK_UNSPECIFIED, buf, 10, 0); + EXPECT_GE(fd, 0); + expectAnswersNotValid(fd, -EINVAL); + + std::vector largeBuf(2 * MAXPACKET, 0); + // A buffer larger than 8KB + fd = android_res_nsend( + NETWORK_UNSPECIFIED, largeBuf.data(), largeBuf.size(), 0); + EXPECT_EQ(-EMSGSIZE, fd); + + // 5000 bytes filled with 0. This returns EMSGSIZE because FrameworkListener limits the size of + // commands to 4096 bytes. + fd = android_res_nsend(NETWORK_UNSPECIFIED, largeBuf.data(), 5000, 0); + EXPECT_EQ(-EMSGSIZE, fd); + + // 500 bytes filled with 0 + fd = android_res_nsend(NETWORK_UNSPECIFIED, largeBuf.data(), 500, 0); + EXPECT_GE(fd, 0); + expectAnswersNotValid(fd, -EINVAL); + + // 5000 bytes filled with 0xFF + std::vector ffBuf(5000, 0xFF); + fd = android_res_nsend( + NETWORK_UNSPECIFIED, ffBuf.data(), ffBuf.size(), 0); + EXPECT_EQ(-EMSGSIZE, fd); + + // 500 bytes filled with 0xFF + fd = android_res_nsend(NETWORK_UNSPECIFIED, ffBuf.data(), 500, 0); + EXPECT_GE(fd, 0); + expectAnswersNotValid(fd, -EINVAL); +} diff --git a/tests/cts/net/native/qtaguid/Android.bp b/tests/cts/net/native/qtaguid/Android.bp new file mode 100644 index 0000000000..4861651504 --- /dev/null +++ b/tests/cts/net/native/qtaguid/Android.bp @@ -0,0 +1,53 @@ +// 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. + +// Build the unit tests. + +cc_test { + name: "CtsNativeNetTestCases", + + compile_multilib: "both", + multilib: { + lib32: { + suffix: "32", + }, + lib64: { + suffix: "64", + }, + }, + + srcs: ["src/NativeQtaguidTest.cpp"], + + shared_libs: [ + "libutils", + "liblog", + ], + + static_libs: [ + "libgtest", + "libqtaguid", + ], + + // Tag this module as a cts test artifact + test_suites: [ + "cts", + "general-tests", + ], + + cflags: [ + "-Werror", + "-Wall", + ], + +} diff --git a/tests/cts/net/native/qtaguid/AndroidTest.xml b/tests/cts/net/native/qtaguid/AndroidTest.xml new file mode 100644 index 0000000000..fa4b2cf577 --- /dev/null +++ b/tests/cts/net/native/qtaguid/AndroidTest.xml @@ -0,0 +1,32 @@ + + + + diff --git a/tests/cts/net/native/qtaguid/src/NativeQtaguidTest.cpp b/tests/cts/net/native/qtaguid/src/NativeQtaguidTest.cpp new file mode 100644 index 0000000000..7dc6240667 --- /dev/null +++ b/tests/cts/net/native/qtaguid/src/NativeQtaguidTest.cpp @@ -0,0 +1,130 @@ +/* + * 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. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +int canAccessQtaguidFile() { + int fd = open("/proc/net/xt_qtaguid/ctrl", O_RDONLY | O_CLOEXEC); + close(fd); + return fd != -1; +} + +#define SKIP_IF_QTAGUID_NOT_SUPPORTED() \ + do { \ + int res = canAccessQtaguidFile(); \ + ASSERT_LE(0, res); \ + if (!res) { \ + GTEST_LOG_(INFO) << "This test is skipped since kernel may not have the module\n"; \ + return; \ + } \ + } while (0) + +int getCtrlSkInfo(int tag, uid_t uid, uint64_t* sk_addr, int* ref_cnt) { + FILE *fp; + fp = fopen("/proc/net/xt_qtaguid/ctrl", "r"); + if (!fp) + return -ENOENT; + uint64_t full_tag = (uint64_t)tag << 32 | uid; + char pattern[40]; + snprintf(pattern, sizeof(pattern), " tag=0x%" PRIx64 " (uid=%" PRIu32 ")", full_tag, uid); + + size_t len; + char *line_buffer = NULL; + while(getline(&line_buffer, &len, fp) != -1) { + if (strstr(line_buffer, pattern) == NULL) + continue; + int res; + pid_t dummy_pid; + uint64_t k_tag; + uint32_t k_uid; + const int TOTAL_PARAM = 5; + res = sscanf(line_buffer, "sock=%" PRIx64 " tag=0x%" PRIx64 " (uid=%" PRIu32 ") " + "pid=%u f_count=%u", sk_addr, &k_tag, &k_uid, + &dummy_pid, ref_cnt); + if (!(res == TOTAL_PARAM && k_tag == full_tag && k_uid == uid)) + return -EINVAL; + free(line_buffer); + return 0; + } + free(line_buffer); + return -ENOENT; +} + +void checkNoSocketPointerLeaks(int family) { + int sockfd = socket(family, SOCK_STREAM, 0); + uid_t uid = getuid(); + int tag = arc4random(); + int ref_cnt; + uint64_t sk_addr; + uint64_t expect_addr = 0; + + EXPECT_EQ(0, legacy_tagSocket(sockfd, tag, uid)); + EXPECT_EQ(0, getCtrlSkInfo(tag, uid, &sk_addr, &ref_cnt)); + EXPECT_EQ(expect_addr, sk_addr); + close(sockfd); + EXPECT_EQ(-ENOENT, getCtrlSkInfo(tag, uid, &sk_addr, &ref_cnt)); +} + +TEST (NativeQtaguidTest, close_socket_without_untag) { + SKIP_IF_QTAGUID_NOT_SUPPORTED(); + + int sockfd = socket(AF_INET, SOCK_STREAM, 0); + uid_t uid = getuid(); + int tag = arc4random(); + int ref_cnt; + uint64_t dummy_sk; + EXPECT_EQ(0, legacy_tagSocket(sockfd, tag, uid)); + EXPECT_EQ(0, getCtrlSkInfo(tag, uid, &dummy_sk, &ref_cnt)); + EXPECT_EQ(2, ref_cnt); + close(sockfd); + EXPECT_EQ(-ENOENT, getCtrlSkInfo(tag, uid, &dummy_sk, &ref_cnt)); +} + +TEST (NativeQtaguidTest, close_socket_without_untag_ipv6) { + SKIP_IF_QTAGUID_NOT_SUPPORTED(); + + int sockfd = socket(AF_INET6, SOCK_STREAM, 0); + uid_t uid = getuid(); + int tag = arc4random(); + int ref_cnt; + uint64_t dummy_sk; + EXPECT_EQ(0, legacy_tagSocket(sockfd, tag, uid)); + EXPECT_EQ(0, getCtrlSkInfo(tag, uid, &dummy_sk, &ref_cnt)); + EXPECT_EQ(2, ref_cnt); + close(sockfd); + EXPECT_EQ(-ENOENT, getCtrlSkInfo(tag, uid, &dummy_sk, &ref_cnt)); +} + +TEST (NativeQtaguidTest, no_socket_addr_leak) { + SKIP_IF_QTAGUID_NOT_SUPPORTED(); + + checkNoSocketPointerLeaks(AF_INET); + checkNoSocketPointerLeaks(AF_INET6); +} + +int main(int argc, char **argv) { + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/cts/net/src/android/net/cts/AirplaneModeTest.java b/tests/cts/net/src/android/net/cts/AirplaneModeTest.java new file mode 100644 index 0000000000..524e549ab7 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/AirplaneModeTest.java @@ -0,0 +1,86 @@ +/* + * 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.cts; + +import android.content.ContentResolver; +import android.content.Context; +import android.platform.test.annotations.AppModeFull; +import android.provider.Settings; +import android.test.AndroidTestCase; +import android.util.Log; + +import java.lang.Thread; + +@AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps") +public class AirplaneModeTest extends AndroidTestCase { + private static final String TAG = "AirplaneModeTest"; + private static final String FEATURE_BLUETOOTH = "android.hardware.bluetooth"; + private static final String FEATURE_WIFI = "android.hardware.wifi"; + private static final int TIMEOUT_MS = 10 * 1000; + private boolean mHasFeature; + private Context mContext; + private ContentResolver resolver; + + public void setup() { + mContext= getContext(); + resolver = mContext.getContentResolver(); + mHasFeature = (mContext.getPackageManager().hasSystemFeature(FEATURE_BLUETOOTH) + || mContext.getPackageManager().hasSystemFeature(FEATURE_WIFI)); + } + + public void testAirplaneMode() { + setup(); + if (!mHasFeature) { + Log.i(TAG, "The device doesn't support network bluetooth or wifi feature"); + return; + } + + for (int testCount = 0; testCount < 2; testCount++) { + if (!doOneTest()) { + fail("Airplane mode failed to change in " + TIMEOUT_MS + "msec"); + return; + } + } + } + + private boolean doOneTest() { + boolean airplaneModeOn = isAirplaneModeOn(); + setAirplaneModeOn(!airplaneModeOn); + + try { + Thread.sleep(TIMEOUT_MS); + } catch (InterruptedException e) { + Log.e(TAG, "Sleep time interrupted.", e); + } + + if (airplaneModeOn == isAirplaneModeOn()) { + return false; + } + return true; + } + + private void setAirplaneModeOn(boolean enabling) { + // Change the system setting for airplane mode + Settings.Global.putInt(resolver, Settings.Global.AIRPLANE_MODE_ON, enabling ? 1 : 0); + } + + private boolean isAirplaneModeOn() { + // Read the system setting for airplane mode + return Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.AIRPLANE_MODE_ON, 0) != 0; + } +} diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt new file mode 100644 index 0000000000..eb5048fa9b --- /dev/null +++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt @@ -0,0 +1,194 @@ +/* + * 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.cts + +import android.Manifest.permission.CONNECTIVITY_INTERNAL +import android.Manifest.permission.NETWORK_SETTINGS +import android.Manifest.permission.READ_DEVICE_CONFIG +import android.content.pm.PackageManager.FEATURE_TELEPHONY +import android.content.pm.PackageManager.FEATURE_WIFI +import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL +import android.net.NetworkCapabilities.TRANSPORT_WIFI +import android.net.NetworkRequest +import android.net.Uri +import android.net.cts.NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig +import android.net.cts.NetworkValidationTestUtil.setHttpUrlDeviceConfig +import android.net.cts.NetworkValidationTestUtil.setHttpsUrlDeviceConfig +import android.net.cts.NetworkValidationTestUtil.setUrlExpirationDeviceConfig +import android.net.cts.util.CtsNetUtils +import android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL +import android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL +import android.net.wifi.WifiManager +import android.os.Build +import android.platform.test.annotations.AppModeFull +import android.provider.DeviceConfig +import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY +import android.text.TextUtils +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.TestHttpServer +import com.android.testutils.TestHttpServer.Request +import com.android.testutils.isDevSdkInRange +import com.android.testutils.runAsShell +import fi.iki.elonen.NanoHTTPD.Response.Status +import junit.framework.AssertionFailedError +import org.junit.After +import org.junit.Assume.assumeTrue +import org.junit.Before +import org.junit.runner.RunWith +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException +import kotlin.test.Test +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +private const val TEST_HTTPS_URL_PATH = "/https_path" +private const val TEST_HTTP_URL_PATH = "/http_path" +private const val TEST_PORTAL_URL_PATH = "/portal_path" + +private const val LOCALHOST_HOSTNAME = "localhost" + +// Re-connecting to the AP, obtaining an IP address, revalidating can take a long time +private const val WIFI_CONNECT_TIMEOUT_MS = 120_000L +private const val TEST_TIMEOUT_MS = 10_000L + +private fun CompletableFuture.assertGet(timeoutMs: Long, message: String): T { + try { + return get(timeoutMs, TimeUnit.MILLISECONDS) + } catch (e: TimeoutException) { + throw AssertionFailedError(message) + } +} + +@AppModeFull(reason = "WRITE_DEVICE_CONFIG permission can't be granted to instant apps") +@RunWith(AndroidJUnit4::class) +class CaptivePortalTest { + private val context: android.content.Context by lazy { getInstrumentation().context } + private val wm by lazy { context.getSystemService(WifiManager::class.java) } + private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) } + private val pm by lazy { context.packageManager } + private val utils by lazy { CtsNetUtils(context) } + + private val server = TestHttpServer("localhost") + + @Before + fun setUp() { + runAsShell(READ_DEVICE_CONFIG) { + // Verify that the test URLs are not normally set on the device, but do not fail if the + // test URLs are set to what this test uses (URLs on localhost), in case the test was + // interrupted manually and rerun. + assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL) + assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL) + } + clearValidationTestUrlsDeviceConfig() + server.start() + } + + @After + fun tearDown() { + clearValidationTestUrlsDeviceConfig() + if (pm.hasSystemFeature(FEATURE_WIFI)) { + reconnectWifi() + } + server.stop() + } + + private fun assertEmptyOrLocalhostUrl(urlKey: String) { + val url = DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, urlKey) + assertTrue(TextUtils.isEmpty(url) || LOCALHOST_HOSTNAME == Uri.parse(url).host, + "$urlKey must not be set in production scenarios (current value: $url)") + } + + @Test + fun testCaptivePortalIsNotDefaultNetwork() { + assumeTrue(pm.hasSystemFeature(FEATURE_TELEPHONY)) + assumeTrue(pm.hasSystemFeature(FEATURE_WIFI)) + utils.ensureWifiConnected() + utils.connectToCell() + + // Have network validation use a local server that serves a HTTPS error / HTTP redirect + server.addResponse(Request(TEST_PORTAL_URL_PATH), Status.OK, + content = "Test captive portal content") + server.addResponse(Request(TEST_HTTPS_URL_PATH), Status.INTERNAL_ERROR) + server.addResponse(Request(TEST_HTTP_URL_PATH), Status.REDIRECT, + locationHeader = makeUrl(TEST_PORTAL_URL_PATH)) + setHttpsUrlDeviceConfig(makeUrl(TEST_HTTPS_URL_PATH)) + setHttpUrlDeviceConfig(makeUrl(TEST_HTTP_URL_PATH)) + // URL expiration needs to be in the next 10 minutes + setUrlExpirationDeviceConfig(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(9)) + + // Wait for a captive portal to be detected on the network + val wifiNetworkFuture = CompletableFuture() + val wifiCb = object : NetworkCallback() { + override fun onCapabilitiesChanged( + network: Network, + nc: NetworkCapabilities + ) { + if (nc.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) { + wifiNetworkFuture.complete(network) + } + } + } + cm.requestNetwork(NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(), wifiCb) + + try { + reconnectWifi() + val network = wifiNetworkFuture.assertGet(WIFI_CONNECT_TIMEOUT_MS, + "Captive portal not detected after ${WIFI_CONNECT_TIMEOUT_MS}ms") + + val wifiDefaultMessage = "Wifi should not be the default network when a captive " + + "portal was detected and another network (mobile data) can provide internet " + + "access." + assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage) + + val startPortalAppPermission = + if (isDevSdkInRange(0, Build.VERSION_CODES.Q)) CONNECTIVITY_INTERNAL + else NETWORK_SETTINGS + runAsShell(startPortalAppPermission) { cm.startCaptivePortalApp(network) } + + // Expect the portal content to be fetched at some point after detecting the portal. + // Some implementations may fetch the URL before startCaptivePortalApp is called. + assertNotNull(server.requestsRecord.poll(TEST_TIMEOUT_MS, pos = 0) { + it.path == TEST_PORTAL_URL_PATH + }, "The captive portal login page was still not fetched ${TEST_TIMEOUT_MS}ms " + + "after startCaptivePortalApp.") + + assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage) + } finally { + cm.unregisterNetworkCallback(wifiCb) + server.stop() + // disconnectFromCell should be called after connectToCell + utils.disconnectFromCell() + } + } + + /** + * Create a URL string that, when fetched, will hit the test server with the given URL [path]. + */ + private fun makeUrl(path: String) = "http://localhost:${server.listeningPort}" + path + + private fun reconnectWifi() { + utils.ensureWifiDisconnected(null /* wifiNetworkToCheck */) + utils.ensureWifiConnected() + } +} \ No newline at end of file diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java new file mode 100644 index 0000000000..54509cd1df --- /dev/null +++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java @@ -0,0 +1,576 @@ +/* + * 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.cts; + +import static android.content.pm.PackageManager.FEATURE_TELEPHONY; +import static android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback; +import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport; +import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_ATTEMPTED_BITMASK; +import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_SUCCEEDED_BITMASK; +import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_VALIDATION_RESULT; +import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.NETWORK_VALIDATION_RESULT_VALID; +import static android.net.ConnectivityDiagnosticsManager.DataStallReport; +import static android.net.ConnectivityDiagnosticsManager.DataStallReport.DETECTION_METHOD_DNS_EVENTS; +import static android.net.ConnectivityDiagnosticsManager.DataStallReport.DETECTION_METHOD_TCP_METRICS; +import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_DNS_CONSECUTIVE_TIMEOUTS; +import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS; +import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_PACKET_FAIL_RATE; +import static android.net.ConnectivityDiagnosticsManager.persistableBundleEquals; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_TEST; +import static android.net.cts.util.CtsNetUtils.TestNetworkCallback; + +import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity; +import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; + +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 static org.junit.Assume.assumeTrue; + +import android.annotation.NonNull; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.net.ConnectivityDiagnosticsManager; +import android.net.ConnectivityManager; +import android.net.LinkAddress; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.TestNetworkInterface; +import android.net.TestNetworkManager; +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.PersistableBundle; +import android.os.Process; +import android.platform.test.annotations.AppModeFull; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.util.Pair; + +import androidx.test.InstrumentationRegistry; + +import com.android.internal.telephony.uicc.IccUtils; +import com.android.internal.util.ArrayUtils; +import com.android.net.module.util.ArrayTrackRecord; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; +import com.android.testutils.DevSdkIgnoreRunner; +import com.android.testutils.SkipPresubmit; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +@RunWith(DevSdkIgnoreRunner.class) +@IgnoreUpTo(Build.VERSION_CODES.Q) // ConnectivityDiagnosticsManager did not exist in Q +@AppModeFull(reason = "CHANGE_NETWORK_STATE, MANAGE_TEST_NETWORKS not grantable to instant apps") +public class ConnectivityDiagnosticsManagerTest { + private static final int CALLBACK_TIMEOUT_MILLIS = 5000; + private static final int NO_CALLBACK_INVOKED_TIMEOUT = 500; + private static final long TIMESTAMP = 123456789L; + private static final int DNS_CONSECUTIVE_TIMEOUTS = 5; + private static final int COLLECTION_PERIOD_MILLIS = 5000; + private static final int FAIL_RATE_PERCENTAGE = 100; + private static final int UNKNOWN_DETECTION_METHOD = 4; + private static final int FILTERED_UNKNOWN_DETECTION_METHOD = 0; + private static final int CARRIER_CONFIG_CHANGED_BROADCAST_TIMEOUT = 5000; + private static final int DELAY_FOR_ADMIN_UIDS_MILLIS = 2000; + + private static final Executor INLINE_EXECUTOR = x -> x.run(); + + private static final NetworkRequest TEST_NETWORK_REQUEST = + new NetworkRequest.Builder() + .addTransportType(TRANSPORT_TEST) + .removeCapability(NET_CAPABILITY_TRUSTED) + .removeCapability(NET_CAPABILITY_NOT_VPN) + .build(); + + private static final String SHA_256 = "SHA-256"; + + private static final NetworkRequest CELLULAR_NETWORK_REQUEST = + new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_INTERNET) + .build(); + + private static final IBinder BINDER = new Binder(); + + private Context mContext; + private ConnectivityManager mConnectivityManager; + private ConnectivityDiagnosticsManager mCdm; + private CarrierConfigManager mCarrierConfigManager; + private PackageManager mPackageManager; + private TelephonyManager mTelephonyManager; + + // Callback used to keep TestNetworks up when there are no other outstanding NetworkRequests + // for it. + private TestNetworkCallback mTestNetworkCallback; + private Network mTestNetwork; + private ParcelFileDescriptor mTestNetworkFD; + + private List mRegisteredCallbacks; + + @Before + public void setUp() throws Exception { + mContext = InstrumentationRegistry.getContext(); + mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); + mCdm = mContext.getSystemService(ConnectivityDiagnosticsManager.class); + mCarrierConfigManager = mContext.getSystemService(CarrierConfigManager.class); + mPackageManager = mContext.getPackageManager(); + mTelephonyManager = mContext.getSystemService(TelephonyManager.class); + + mTestNetworkCallback = new TestNetworkCallback(); + mConnectivityManager.requestNetwork(TEST_NETWORK_REQUEST, mTestNetworkCallback); + + mRegisteredCallbacks = new ArrayList<>(); + } + + @After + public void tearDown() throws Exception { + mConnectivityManager.unregisterNetworkCallback(mTestNetworkCallback); + if (mTestNetwork != null) { + runWithShellPermissionIdentity(() -> { + final TestNetworkManager tnm = mContext.getSystemService(TestNetworkManager.class); + tnm.teardownTestNetwork(mTestNetwork); + }); + mTestNetwork = null; + } + + if (mTestNetworkFD != null) { + mTestNetworkFD.close(); + mTestNetworkFD = null; + } + + for (TestConnectivityDiagnosticsCallback cb : mRegisteredCallbacks) { + mCdm.unregisterConnectivityDiagnosticsCallback(cb); + } + } + + @Test + public void testRegisterConnectivityDiagnosticsCallback() throws Exception { + mTestNetworkFD = setUpTestNetwork().getFileDescriptor(); + mTestNetwork = mTestNetworkCallback.waitForAvailable(); + + final TestConnectivityDiagnosticsCallback cb = + createAndRegisterConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST); + + final String interfaceName = + mConnectivityManager.getLinkProperties(mTestNetwork).getInterfaceName(); + + cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName); + cb.assertNoCallback(); + } + + @SkipPresubmit(reason = "Flaky: b/159718782; add to presubmit after fixing") + @Test + public void testRegisterCallbackWithCarrierPrivileges() throws Exception { + assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY)); + + final int subId = SubscriptionManager.getDefaultSubscriptionId(); + if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + fail("Need an active subscription. Please ensure that the device has working mobile" + + " data."); + } + + final CarrierConfigReceiver carrierConfigReceiver = new CarrierConfigReceiver(subId); + mContext.registerReceiver( + carrierConfigReceiver, + new IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)); + + final TestNetworkCallback testNetworkCallback = new TestNetworkCallback(); + + try { + doBroadcastCarrierConfigsAndVerifyOnConnectivityReportAvailable( + subId, carrierConfigReceiver, testNetworkCallback); + } finally { + runWithShellPermissionIdentity( + () -> mCarrierConfigManager.overrideConfig(subId, null), + android.Manifest.permission.MODIFY_PHONE_STATE); + mConnectivityManager.unregisterNetworkCallback(testNetworkCallback); + mContext.unregisterReceiver(carrierConfigReceiver); + } + } + + private String getCertHashForThisPackage() throws Exception { + final PackageInfo pkgInfo = + mPackageManager.getPackageInfo( + mContext.getOpPackageName(), PackageManager.GET_SIGNATURES); + final MessageDigest md = MessageDigest.getInstance(SHA_256); + final byte[] certHash = md.digest(pkgInfo.signatures[0].toByteArray()); + return IccUtils.bytesToHexString(certHash); + } + + private void doBroadcastCarrierConfigsAndVerifyOnConnectivityReportAvailable( + int subId, + @NonNull CarrierConfigReceiver carrierConfigReceiver, + @NonNull TestNetworkCallback testNetworkCallback) + throws Exception { + final PersistableBundle carrierConfigs = new PersistableBundle(); + carrierConfigs.putStringArray( + CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY, + new String[] {getCertHashForThisPackage()}); + + runWithShellPermissionIdentity( + () -> { + mCarrierConfigManager.overrideConfig(subId, carrierConfigs); + mCarrierConfigManager.notifyConfigChangedForSubId(subId); + }, + android.Manifest.permission.MODIFY_PHONE_STATE); + + // TODO(b/157779832): This should use android.permission.CHANGE_NETWORK_STATE. However, the + // shell does not have CHANGE_NETWORK_STATE, so use CONNECTIVITY_INTERNAL until the shell + // permissions are updated. + runWithShellPermissionIdentity( + () -> mConnectivityManager.requestNetwork( + CELLULAR_NETWORK_REQUEST, testNetworkCallback), + android.Manifest.permission.CONNECTIVITY_INTERNAL); + + final Network network = testNetworkCallback.waitForAvailable(); + assertNotNull(network); + + assertTrue("Didn't receive broadcast for ACTION_CARRIER_CONFIG_CHANGED for subId=" + subId, + carrierConfigReceiver.waitForCarrierConfigChanged()); + assertTrue("Don't have Carrier Privileges after adding cert for this package", + mTelephonyManager.createForSubscriptionId(subId).hasCarrierPrivileges()); + + // Wait for CarrierPrivilegesTracker to receive the ACTION_CARRIER_CONFIG_CHANGED + // broadcast. CPT then needs to update the corresponding DataConnection, which then + // updates ConnectivityService. Unfortunately, this update to the NetworkCapabilities in + // CS does not trigger NetworkCallback#onCapabilitiesChanged as changing the + // administratorUids is not a publicly visible change. In lieu of a better signal to + // detministically wait for, use Thread#sleep here. + // TODO(b/157949581): replace this Thread#sleep with a deterministic signal + Thread.sleep(DELAY_FOR_ADMIN_UIDS_MILLIS); + + final TestConnectivityDiagnosticsCallback connDiagsCallback = + createAndRegisterConnectivityDiagnosticsCallback(CELLULAR_NETWORK_REQUEST); + + final String interfaceName = + mConnectivityManager.getLinkProperties(network).getInterfaceName(); + connDiagsCallback.expectOnConnectivityReportAvailable( + network, interfaceName, TRANSPORT_CELLULAR); + connDiagsCallback.assertNoCallback(); + } + + @Test + public void testRegisterDuplicateConnectivityDiagnosticsCallback() { + final TestConnectivityDiagnosticsCallback cb = + createAndRegisterConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST); + + try { + mCdm.registerConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST, INLINE_EXECUTOR, cb); + fail("Registering the same callback twice should throw an IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testUnregisterConnectivityDiagnosticsCallback() { + final TestConnectivityDiagnosticsCallback cb = new TestConnectivityDiagnosticsCallback(); + mCdm.registerConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST, INLINE_EXECUTOR, cb); + mCdm.unregisterConnectivityDiagnosticsCallback(cb); + } + + @Test + public void testUnregisterUnknownConnectivityDiagnosticsCallback() { + // Expected to silently ignore the unregister() call + mCdm.unregisterConnectivityDiagnosticsCallback(new TestConnectivityDiagnosticsCallback()); + } + + @Test + public void testOnConnectivityReportAvailable() throws Exception { + final TestConnectivityDiagnosticsCallback cb = + createAndRegisterConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST); + + mTestNetworkFD = setUpTestNetwork().getFileDescriptor(); + mTestNetwork = mTestNetworkCallback.waitForAvailable(); + + final String interfaceName = + mConnectivityManager.getLinkProperties(mTestNetwork).getInterfaceName(); + + cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName); + cb.assertNoCallback(); + } + + @Test + public void testOnDataStallSuspected_DnsEvents() throws Exception { + final PersistableBundle extras = new PersistableBundle(); + extras.putInt(KEY_DNS_CONSECUTIVE_TIMEOUTS, DNS_CONSECUTIVE_TIMEOUTS); + + verifyOnDataStallSuspected(DETECTION_METHOD_DNS_EVENTS, TIMESTAMP, extras); + } + + @Test + public void testOnDataStallSuspected_TcpMetrics() throws Exception { + final PersistableBundle extras = new PersistableBundle(); + extras.putInt(KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS, COLLECTION_PERIOD_MILLIS); + extras.putInt(KEY_TCP_PACKET_FAIL_RATE, FAIL_RATE_PERCENTAGE); + + verifyOnDataStallSuspected(DETECTION_METHOD_TCP_METRICS, TIMESTAMP, extras); + } + + @Test + public void testOnDataStallSuspected_UnknownDetectionMethod() throws Exception { + verifyOnDataStallSuspected( + UNKNOWN_DETECTION_METHOD, + FILTERED_UNKNOWN_DETECTION_METHOD, + TIMESTAMP, + PersistableBundle.EMPTY); + } + + private void verifyOnDataStallSuspected( + int detectionMethod, long timestampMillis, @NonNull PersistableBundle extras) + throws Exception { + // Input detection method is expected to match received detection method + verifyOnDataStallSuspected(detectionMethod, detectionMethod, timestampMillis, extras); + } + + private void verifyOnDataStallSuspected( + int inputDetectionMethod, + int expectedDetectionMethod, + long timestampMillis, + @NonNull PersistableBundle extras) + throws Exception { + mTestNetworkFD = setUpTestNetwork().getFileDescriptor(); + mTestNetwork = mTestNetworkCallback.waitForAvailable(); + + final TestConnectivityDiagnosticsCallback cb = + createAndRegisterConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST); + + final String interfaceName = + mConnectivityManager.getLinkProperties(mTestNetwork).getInterfaceName(); + + cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName); + + runWithShellPermissionIdentity( + () -> mConnectivityManager.simulateDataStall( + inputDetectionMethod, timestampMillis, mTestNetwork, extras), + android.Manifest.permission.MANAGE_TEST_NETWORKS); + + cb.expectOnDataStallSuspected( + mTestNetwork, interfaceName, expectedDetectionMethod, timestampMillis, extras); + cb.assertNoCallback(); + } + + @Test + public void testOnNetworkConnectivityReportedTrue() throws Exception { + verifyOnNetworkConnectivityReported(true /* hasConnectivity */); + } + + @Test + public void testOnNetworkConnectivityReportedFalse() throws Exception { + verifyOnNetworkConnectivityReported(false /* hasConnectivity */); + } + + private void verifyOnNetworkConnectivityReported(boolean hasConnectivity) throws Exception { + mTestNetworkFD = setUpTestNetwork().getFileDescriptor(); + mTestNetwork = mTestNetworkCallback.waitForAvailable(); + + final TestConnectivityDiagnosticsCallback cb = + createAndRegisterConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST); + + // onConnectivityReportAvailable always invoked when the test network is established + final String interfaceName = + mConnectivityManager.getLinkProperties(mTestNetwork).getInterfaceName(); + cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName); + cb.assertNoCallback(); + + mConnectivityManager.reportNetworkConnectivity(mTestNetwork, hasConnectivity); + + cb.expectOnNetworkConnectivityReported(mTestNetwork, hasConnectivity); + + // if hasConnectivity does not match the network's known connectivity, it will be + // revalidated which will trigger another onConnectivityReportAvailable callback. + if (!hasConnectivity) { + cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName); + } + + cb.assertNoCallback(); + } + + private TestConnectivityDiagnosticsCallback createAndRegisterConnectivityDiagnosticsCallback( + NetworkRequest request) { + final TestConnectivityDiagnosticsCallback cb = new TestConnectivityDiagnosticsCallback(); + mCdm.registerConnectivityDiagnosticsCallback(request, INLINE_EXECUTOR, cb); + mRegisteredCallbacks.add(cb); + return cb; + } + + /** + * Registers a test NetworkAgent with ConnectivityService with limited capabilities, which leads + * to the Network being validated. + */ + @NonNull + private TestNetworkInterface setUpTestNetwork() throws Exception { + final int[] administratorUids = new int[] {Process.myUid()}; + return callWithShellPermissionIdentity( + () -> { + final TestNetworkManager tnm = + mContext.getSystemService(TestNetworkManager.class); + final TestNetworkInterface tni = tnm.createTunInterface(new LinkAddress[0]); + tnm.setupTestNetwork(tni.getInterfaceName(), administratorUids, BINDER); + return tni; + }); + } + + private static class TestConnectivityDiagnosticsCallback + extends ConnectivityDiagnosticsCallback { + private final ArrayTrackRecord.ReadHead mHistory = + new ArrayTrackRecord().newReadHead(); + + @Override + public void onConnectivityReportAvailable(ConnectivityReport report) { + mHistory.add(report); + } + + @Override + public void onDataStallSuspected(DataStallReport report) { + mHistory.add(report); + } + + @Override + public void onNetworkConnectivityReported(Network network, boolean hasConnectivity) { + mHistory.add(new Pair(network, hasConnectivity)); + } + + public void expectOnConnectivityReportAvailable( + @NonNull Network network, @NonNull String interfaceName) { + expectOnConnectivityReportAvailable(network, interfaceName, TRANSPORT_TEST); + } + + public void expectOnConnectivityReportAvailable( + @NonNull Network network, @NonNull String interfaceName, int transportType) { + final ConnectivityReport result = + (ConnectivityReport) mHistory.poll(CALLBACK_TIMEOUT_MILLIS, x -> true); + assertEquals(network, result.getNetwork()); + + final NetworkCapabilities nc = result.getNetworkCapabilities(); + assertNotNull(nc); + assertTrue(nc.hasTransport(transportType)); + assertNotNull(result.getLinkProperties()); + assertEquals(interfaceName, result.getLinkProperties().getInterfaceName()); + + final PersistableBundle extras = result.getAdditionalInfo(); + assertTrue(extras.containsKey(KEY_NETWORK_VALIDATION_RESULT)); + final int validationResult = extras.getInt(KEY_NETWORK_VALIDATION_RESULT); + assertEquals("Network validation result is not 'valid'", + NETWORK_VALIDATION_RESULT_VALID, validationResult); + + assertTrue(extras.containsKey(KEY_NETWORK_PROBES_SUCCEEDED_BITMASK)); + final int probesSucceeded = extras.getInt(KEY_NETWORK_VALIDATION_RESULT); + assertTrue("PROBES_SUCCEEDED mask not in expected range", probesSucceeded >= 0); + + assertTrue(extras.containsKey(KEY_NETWORK_PROBES_ATTEMPTED_BITMASK)); + final int probesAttempted = extras.getInt(KEY_NETWORK_PROBES_ATTEMPTED_BITMASK); + assertTrue("PROBES_ATTEMPTED mask not in expected range", probesAttempted >= 0); + } + + public void expectOnDataStallSuspected( + @NonNull Network network, + @NonNull String interfaceName, + int detectionMethod, + long timestampMillis, + @NonNull PersistableBundle extras) { + final DataStallReport result = + (DataStallReport) mHistory.poll(CALLBACK_TIMEOUT_MILLIS, x -> true); + assertEquals(network, result.getNetwork()); + assertEquals(detectionMethod, result.getDetectionMethod()); + assertEquals(timestampMillis, result.getReportTimestamp()); + + final NetworkCapabilities nc = result.getNetworkCapabilities(); + assertNotNull(nc); + assertTrue(nc.hasTransport(TRANSPORT_TEST)); + assertNotNull(result.getLinkProperties()); + assertEquals(interfaceName, result.getLinkProperties().getInterfaceName()); + + assertTrue(persistableBundleEquals(extras, result.getStallDetails())); + } + + public void expectOnNetworkConnectivityReported( + @NonNull Network network, boolean hasConnectivity) { + final Pair result = + (Pair) mHistory.poll(CALLBACK_TIMEOUT_MILLIS, x -> true); + assertEquals(network, result.first /* network */); + assertEquals(hasConnectivity, result.second /* hasConnectivity */); + } + + public void assertNoCallback() { + // If no more callbacks exist, there should be nothing left in the ReadHead + assertNull("Unexpected event in history", + mHistory.poll(NO_CALLBACK_INVOKED_TIMEOUT, x -> true)); + } + } + + private class CarrierConfigReceiver extends BroadcastReceiver { + private final CountDownLatch mLatch = new CountDownLatch(1); + private final int mSubId; + + CarrierConfigReceiver(int subId) { + mSubId = subId; + } + + @Override + public void onReceive(Context context, Intent intent) { + if (!CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(intent.getAction())) { + return; + } + + final int subId = + intent.getIntExtra( + CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX, + SubscriptionManager.INVALID_SUBSCRIPTION_ID); + if (mSubId != subId) return; + + final PersistableBundle carrierConfigs = mCarrierConfigManager.getConfigForSubId(subId); + if (!CarrierConfigManager.isConfigForIdentifiedCarrier(carrierConfigs)) return; + + final String[] certs = + carrierConfigs.getStringArray( + CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY); + try { + if (ArrayUtils.contains(certs, getCertHashForThisPackage())) { + mLatch.countDown(); + } + } catch (Exception e) { + } + } + + boolean waitForCarrierConfigChanged() throws Exception { + return mLatch.await(CARRIER_CONFIG_CHANGED_BROADCAST_TIMEOUT, TimeUnit.MILLISECONDS); + } + } +} diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java new file mode 100644 index 0000000000..db4e3e744a --- /dev/null +++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java @@ -0,0 +1,1530 @@ +/* + * 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.cts; + +import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS; +import static android.Manifest.permission.NETWORK_SETTINGS; +import static android.content.pm.PackageManager.FEATURE_ETHERNET; +import static android.content.pm.PackageManager.FEATURE_TELEPHONY; +import static android.content.pm.PackageManager.FEATURE_USB_HOST; +import static android.content.pm.PackageManager.FEATURE_WIFI; +import static android.content.pm.PackageManager.GET_PERMISSIONS; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.cts.util.CtsNetUtils.ConnectivityActionReceiver; +import static android.net.cts.util.CtsNetUtils.HTTP_PORT; +import static android.net.cts.util.CtsNetUtils.NETWORK_CALLBACK_ACTION; +import static android.net.cts.util.CtsNetUtils.TEST_HOST; +import static android.net.cts.util.CtsNetUtils.TestNetworkCallback; +import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT; +import static android.provider.Settings.Global.NETWORK_METERED_MULTIPATH_PREFERENCE; +import static android.system.OsConstants.AF_INET; +import static android.system.OsConstants.AF_INET6; +import static android.system.OsConstants.AF_UNSPEC; + +import static com.android.compatibility.common.util.SystemUtil.runShellCommand; +import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; +import static com.android.testutils.TestPermissionUtil.runAsShell; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +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.annotation.NonNull; +import android.app.Instrumentation; +import android.app.PendingIntent; +import android.app.UiAutomation; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.IpSecManager; +import android.net.IpSecManager.UdpEncapsulationSocket; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkConfig; +import android.net.NetworkInfo; +import android.net.NetworkInfo.DetailedState; +import android.net.NetworkInfo.State; +import android.net.NetworkRequest; +import android.net.NetworkUtils; +import android.net.SocketKeepalive; +import android.net.cts.util.CtsNetUtils; +import android.net.util.KeepaliveUtils; +import android.net.wifi.WifiManager; +import android.os.Binder; +import android.os.Build; +import android.os.Looper; +import android.os.MessageQueue; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.os.VintfRuntimeInfo; +import android.platform.test.annotations.AppModeFull; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.ArrayUtils; +import com.android.testutils.RecorderCallback.CallbackEntry; +import com.android.testutils.SkipPresubmit; +import com.android.testutils.TestableNetworkCallback; + +import libcore.io.Streams; + +import junit.framework.AssertionFailedError; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.Socket; +import java.net.URL; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@RunWith(AndroidJUnit4.class) +public class ConnectivityManagerTest { + + private static final String TAG = ConnectivityManagerTest.class.getSimpleName(); + + public static final int TYPE_MOBILE = ConnectivityManager.TYPE_MOBILE; + public static final int TYPE_WIFI = ConnectivityManager.TYPE_WIFI; + + private static final int HOST_ADDRESS = 0x7f000001;// represent ip 127.0.0.1 + private static final int KEEPALIVE_CALLBACK_TIMEOUT_MS = 2000; + private static final int INTERVAL_KEEPALIVE_RETRY_MS = 500; + private static final int MAX_KEEPALIVE_RETRY_COUNT = 3; + private static final int MIN_KEEPALIVE_INTERVAL = 10; + + // Changing meteredness on wifi involves reconnecting, which can take several seconds (involves + // re-associating, DHCP...) + private static final int NETWORK_CHANGE_METEREDNESS_TIMEOUT = 30_000; + private static final int NUM_TRIES_MULTIPATH_PREF_CHECK = 20; + private static final long INTERVAL_MULTIPATH_PREF_CHECK_MS = 500; + // device could have only one interface: data, wifi. + private static final int MIN_NUM_NETWORK_TYPES = 1; + + // Airplane Mode BroadcastReceiver Timeout + private static final long AIRPLANE_MODE_CHANGE_TIMEOUT_MS = 10_000L; + + // Minimum supported keepalive counts for wifi and cellular. + public static final int MIN_SUPPORTED_CELLULAR_KEEPALIVE_COUNT = 1; + public static final int MIN_SUPPORTED_WIFI_KEEPALIVE_COUNT = 3; + + private static final String NETWORK_METERED_MULTIPATH_PREFERENCE_RES_NAME = + "config_networkMeteredMultipathPreference"; + private static final String KEEPALIVE_ALLOWED_UNPRIVILEGED_RES_NAME = + "config_allowedUnprivilegedKeepalivePerUid"; + private static final String KEEPALIVE_RESERVED_PER_SLOT_RES_NAME = + "config_reservedPrivilegedKeepaliveSlots"; + + private Context mContext; + private Instrumentation mInstrumentation; + private ConnectivityManager mCm; + private WifiManager mWifiManager; + private PackageManager mPackageManager; + private final HashMap mNetworks = + new HashMap(); + boolean mWifiWasDisabled; + private UiAutomation mUiAutomation; + private CtsNetUtils mCtsNetUtils; + + @Before + public void setUp() throws Exception { + mInstrumentation = InstrumentationRegistry.getInstrumentation(); + mContext = mInstrumentation.getContext(); + mCm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + mPackageManager = mContext.getPackageManager(); + mCtsNetUtils = new CtsNetUtils(mContext); + mWifiWasDisabled = false; + + // Get com.android.internal.R.array.networkAttributes + int resId = mContext.getResources().getIdentifier("networkAttributes", "array", "android"); + String[] naStrings = mContext.getResources().getStringArray(resId); + //TODO: What is the "correct" way to determine if this is a wifi only device? + boolean wifiOnly = SystemProperties.getBoolean("ro.radio.noril", false); + for (String naString : naStrings) { + try { + NetworkConfig n = new NetworkConfig(naString); + if (wifiOnly && ConnectivityManager.isNetworkTypeMobile(n.type)) { + continue; + } + mNetworks.put(n.type, n); + } catch (Exception e) {} + } + mUiAutomation = mInstrumentation.getUiAutomation(); + + assertNotNull("CTS requires a working Internet connection", mCm.getActiveNetwork()); + } + + @After + public void tearDown() throws Exception { + // Return WiFi to its original disabled state after tests that explicitly connect. + if (mWifiWasDisabled) { + mCtsNetUtils.disconnectFromWifi(null); + } + if (mCtsNetUtils.cellConnectAttempted()) { + mCtsNetUtils.disconnectFromCell(); + } + + // All tests in this class require a working Internet connection as they start. Make + // sure there is still one as they end that's ready to use for the next test to use. + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(callback); + try { + assertNotNull("Couldn't restore Internet connectivity", callback.waitForAvailable()); + } finally { + mCm.unregisterNetworkCallback(callback); + } + } + + /** + * Make sure WiFi is connected to an access point if it is not already. If + * WiFi is enabled as a result of this function, it will be disabled + * automatically in tearDown(). + */ + private Network ensureWifiConnected() { + mWifiWasDisabled = !mWifiManager.isWifiEnabled(); + // Even if wifi is enabled, the network may not be connected or ready yet + return mCtsNetUtils.connectToWifi(); + } + + @Test + public void testIsNetworkTypeValid() { + assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE)); + assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_WIFI)); + assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_MMS)); + assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_SUPL)); + assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_DUN)); + assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_HIPRI)); + assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_WIMAX)); + assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_BLUETOOTH)); + assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_DUMMY)); + assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_ETHERNET)); + assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_FOTA)); + assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_IMS)); + assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_CBS)); + assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.TYPE_WIFI_P2P)); + assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE_IA)); + assertFalse(mCm.isNetworkTypeValid(-1)); + assertTrue(mCm.isNetworkTypeValid(0)); + assertTrue(mCm.isNetworkTypeValid(ConnectivityManager.MAX_NETWORK_TYPE)); + assertFalse(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.MAX_NETWORK_TYPE+1)); + + NetworkInfo[] ni = mCm.getAllNetworkInfo(); + + for (NetworkInfo n: ni) { + assertTrue(ConnectivityManager.isNetworkTypeValid(n.getType())); + } + + } + + @Test + public void testSetNetworkPreference() { + // getNetworkPreference() and setNetworkPreference() are both deprecated so they do + // not preform any action. Verify they are at least still callable. + mCm.setNetworkPreference(mCm.getNetworkPreference()); + } + + @Test + public void testGetActiveNetworkInfo() { + NetworkInfo ni = mCm.getActiveNetworkInfo(); + + assertNotNull("You must have an active network connection to complete CTS", ni); + assertTrue(ConnectivityManager.isNetworkTypeValid(ni.getType())); + assertTrue(ni.getState() == State.CONNECTED); + } + + @Test + public void testGetActiveNetwork() { + Network network = mCm.getActiveNetwork(); + assertNotNull("You must have an active network connection to complete CTS", network); + + NetworkInfo ni = mCm.getNetworkInfo(network); + assertNotNull("Network returned from getActiveNetwork was invalid", ni); + + // Similar to testGetActiveNetworkInfo above. + assertTrue(ConnectivityManager.isNetworkTypeValid(ni.getType())); + assertTrue(ni.getState() == State.CONNECTED); + } + + @Test + public void testGetNetworkInfo() { + for (int type = -1; type <= ConnectivityManager.MAX_NETWORK_TYPE+1; type++) { + if (shouldBeSupported(type)) { + NetworkInfo ni = mCm.getNetworkInfo(type); + assertTrue("Info shouldn't be null for " + type, ni != null); + State state = ni.getState(); + assertTrue("Bad state for " + type, State.UNKNOWN.ordinal() >= state.ordinal() + && state.ordinal() >= State.CONNECTING.ordinal()); + DetailedState ds = ni.getDetailedState(); + assertTrue("Bad detailed state for " + type, + DetailedState.FAILED.ordinal() >= ds.ordinal() + && ds.ordinal() >= DetailedState.IDLE.ordinal()); + } else { + assertNull("Info should be null for " + type, mCm.getNetworkInfo(type)); + } + } + } + + @Test + public void testGetAllNetworkInfo() { + NetworkInfo[] ni = mCm.getAllNetworkInfo(); + assertTrue(ni.length >= MIN_NUM_NETWORK_TYPES); + for (int type = 0; type <= ConnectivityManager.MAX_NETWORK_TYPE; type++) { + int desiredFoundCount = (shouldBeSupported(type) ? 1 : 0); + int foundCount = 0; + for (NetworkInfo i : ni) { + if (i.getType() == type) foundCount++; + } + if (foundCount != desiredFoundCount) { + Log.e(TAG, "failure in testGetAllNetworkInfo. Dump of returned NetworkInfos:"); + for (NetworkInfo networkInfo : ni) Log.e(TAG, " " + networkInfo); + } + assertTrue("Unexpected foundCount of " + foundCount + " for type " + type, + foundCount == desiredFoundCount); + } + } + + /** + * Tests that connections can be opened on WiFi and cellphone networks, + * and that they are made from different IP addresses. + */ + @AppModeFull(reason = "Cannot get WifiManager in instant app mode") + @Test + @SkipPresubmit(reason = "Virtual devices use a single internet connection for all networks") + public void testOpenConnection() throws Exception { + boolean canRunTest = mPackageManager.hasSystemFeature(FEATURE_WIFI) + && mPackageManager.hasSystemFeature(FEATURE_TELEPHONY); + if (!canRunTest) { + Log.i(TAG,"testOpenConnection cannot execute unless device supports both WiFi " + + "and a cellular connection"); + return; + } + + Network wifiNetwork = mCtsNetUtils.connectToWifi(); + Network cellNetwork = mCtsNetUtils.connectToCell(); + // This server returns the requestor's IP address as the response body. + URL url = new URL("http://google-ipv6test.appspot.com/ip.js?fmt=text"); + String wifiAddressString = httpGet(wifiNetwork, url); + String cellAddressString = httpGet(cellNetwork, url); + + assertFalse(String.format("Same address '%s' on two different networks (%s, %s)", + wifiAddressString, wifiNetwork, cellNetwork), + wifiAddressString.equals(cellAddressString)); + + // Verify that the IP addresses that the requests appeared to come from are actually on the + // respective networks. + assertOnNetwork(wifiAddressString, wifiNetwork); + assertOnNetwork(cellAddressString, cellNetwork); + + assertFalse("Unexpectedly equal: " + wifiNetwork, wifiNetwork.equals(cellNetwork)); + } + + /** + * Performs a HTTP GET to the specified URL on the specified Network, and returns + * the response body decoded as UTF-8. + */ + private static String httpGet(Network network, URL httpUrl) throws IOException { + HttpURLConnection connection = (HttpURLConnection) network.openConnection(httpUrl); + try { + InputStream inputStream = connection.getInputStream(); + return Streams.readFully(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + } finally { + connection.disconnect(); + } + } + + private void assertOnNetwork(String adressString, Network network) throws UnknownHostException { + InetAddress address = InetAddress.getByName(adressString); + LinkProperties linkProperties = mCm.getLinkProperties(network); + // To make sure that the request went out on the right network, check that + // the IP address seen by the server is assigned to the expected network. + // We can only do this for IPv6 addresses, because in IPv4 we will likely + // have a private IPv4 address, and that won't match what the server sees. + if (address instanceof Inet6Address) { + assertContains(linkProperties.getAddresses(), address); + } + } + + private static void assertContains(Collection collection, T element) { + assertTrue(element + " not found in " + collection, collection.contains(element)); + } + + private void assertStartUsingNetworkFeatureUnsupported(int networkType, String feature) { + try { + mCm.startUsingNetworkFeature(networkType, feature); + fail("startUsingNetworkFeature is no longer supported in the current API version"); + } catch (UnsupportedOperationException expected) {} + } + + private void assertStopUsingNetworkFeatureUnsupported(int networkType, String feature) { + try { + mCm.startUsingNetworkFeature(networkType, feature); + fail("stopUsingNetworkFeature is no longer supported in the current API version"); + } catch (UnsupportedOperationException expected) {} + } + + private void assertRequestRouteToHostUnsupported(int networkType, int hostAddress) { + try { + mCm.requestRouteToHost(networkType, hostAddress); + fail("requestRouteToHost is no longer supported in the current API version"); + } catch (UnsupportedOperationException expected) {} + } + + @Test + public void testStartUsingNetworkFeature() { + + final String invalidateFeature = "invalidateFeature"; + final String mmsFeature = "enableMMS"; + + assertStartUsingNetworkFeatureUnsupported(TYPE_MOBILE, invalidateFeature); + assertStopUsingNetworkFeatureUnsupported(TYPE_MOBILE, invalidateFeature); + assertStartUsingNetworkFeatureUnsupported(TYPE_WIFI, mmsFeature); + } + + private boolean shouldEthernetBeSupported() { + // Instant mode apps aren't allowed to query the Ethernet service due to selinux policies. + // When in instant mode, don't fail if the Ethernet service is available. Instead, rely on + // the fact that Ethernet should be supported if the device has a hardware Ethernet port, or + // if the device can be a USB host and thus can use USB Ethernet adapters. + // + // Note that this test this will still fail in instant mode if a device supports Ethernet + // via other hardware means. We are not currently aware of any such device. + return (mContext.getSystemService(Context.ETHERNET_SERVICE) != null) || + mPackageManager.hasSystemFeature(FEATURE_ETHERNET) || + mPackageManager.hasSystemFeature(FEATURE_USB_HOST); + } + + private boolean shouldBeSupported(int networkType) { + return mNetworks.containsKey(networkType) || + (networkType == ConnectivityManager.TYPE_VPN) || + (networkType == ConnectivityManager.TYPE_ETHERNET && shouldEthernetBeSupported()); + } + + @Test + public void testIsNetworkSupported() { + for (int type = -1; type <= ConnectivityManager.MAX_NETWORK_TYPE; type++) { + boolean supported = mCm.isNetworkSupported(type); + if (shouldBeSupported(type)) { + assertTrue("Network type " + type + " should be supported", supported); + } else { + assertFalse("Network type " + type + " should not be supported", supported); + } + } + } + + @Test + public void testRequestRouteToHost() { + for (int type = -1 ; type <= ConnectivityManager.MAX_NETWORK_TYPE; type++) { + assertRequestRouteToHostUnsupported(type, HOST_ADDRESS); + } + } + + @Test + public void testTest() { + mCm.getBackgroundDataSetting(); + } + + private NetworkRequest makeWifiNetworkRequest() { + return new NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .build(); + } + + private NetworkRequest makeCellNetworkRequest() { + return new NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .build(); + } + + /** + * Exercises both registerNetworkCallback and unregisterNetworkCallback. This checks to + * see if we get a callback for the TRANSPORT_WIFI transport type being available. + * + *

In order to test that a NetworkCallback occurs, we need some change in the network + * state (either a transport or capability is now available). The most straightforward is + * WiFi. We could add a version that uses the telephony data connection but it's not clear + * that it would increase test coverage by much (how many devices have 3G radio but not Wifi?). + */ + @AppModeFull(reason = "Cannot get WifiManager in instant app mode") + @Test + public void testRegisterNetworkCallback() { + if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) { + Log.i(TAG, "testRegisterNetworkCallback cannot execute unless device supports WiFi"); + return; + } + + // We will register for a WIFI network being available or lost. + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(makeWifiNetworkRequest(), callback); + + final TestNetworkCallback defaultTrackingCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(defaultTrackingCallback); + + Network wifiNetwork = null; + + try { + ensureWifiConnected(); + + // Now we should expect to get a network callback about availability of the wifi + // network even if it was already connected as a state-based action when the callback + // is registered. + wifiNetwork = callback.waitForAvailable(); + assertNotNull("Did not receive NetworkCallback.onAvailable for TRANSPORT_WIFI", + wifiNetwork); + + assertNotNull("Did not receive NetworkCallback.onAvailable for any default network", + defaultTrackingCallback.waitForAvailable()); + } catch (InterruptedException e) { + fail("Broadcast receiver or NetworkCallback wait was interrupted."); + } finally { + mCm.unregisterNetworkCallback(callback); + mCm.unregisterNetworkCallback(defaultTrackingCallback); + } + } + + /** + * Tests both registerNetworkCallback and unregisterNetworkCallback similarly to + * {@link #testRegisterNetworkCallback} except that a {@code PendingIntent} is used instead + * of a {@code NetworkCallback}. + */ + @AppModeFull(reason = "Cannot get WifiManager in instant app mode") + @Test + public void testRegisterNetworkCallback_withPendingIntent() { + if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) { + Log.i(TAG, "testRegisterNetworkCallback cannot execute unless device supports WiFi"); + return; + } + + // Create a ConnectivityActionReceiver that has an IntentFilter for our locally defined + // action, NETWORK_CALLBACK_ACTION. + IntentFilter filter = new IntentFilter(); + filter.addAction(NETWORK_CALLBACK_ACTION); + + ConnectivityActionReceiver receiver = new ConnectivityActionReceiver( + mCm, ConnectivityManager.TYPE_WIFI, NetworkInfo.State.CONNECTED); + mContext.registerReceiver(receiver, filter); + + // Create a broadcast PendingIntent for NETWORK_CALLBACK_ACTION. + Intent intent = new Intent(NETWORK_CALLBACK_ACTION); + PendingIntent pendingIntent = PendingIntent.getBroadcast( + mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); + + // We will register for a WIFI network being available or lost. + mCm.registerNetworkCallback(makeWifiNetworkRequest(), pendingIntent); + + try { + ensureWifiConnected(); + + // Now we expect to get the Intent delivered notifying of the availability of the wifi + // network even if it was already connected as a state-based action when the callback + // is registered. + assertTrue("Did not receive expected Intent " + intent + " for TRANSPORT_WIFI", + receiver.waitForState()); + } catch (InterruptedException e) { + fail("Broadcast receiver or NetworkCallback wait was interrupted."); + } finally { + mCm.unregisterNetworkCallback(pendingIntent); + pendingIntent.cancel(); + mContext.unregisterReceiver(receiver); + } + } + + /** + * Exercises the requestNetwork with NetworkCallback API. This checks to + * see if we get a callback for an INTERNET request. + */ + @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps") + @Test + public void testRequestNetworkCallback() { + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.requestNetwork(new NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build(), callback); + + try { + // Wait to get callback for availability of internet + Network internetNetwork = callback.waitForAvailable(); + assertNotNull("Did not receive NetworkCallback#onAvailable for INTERNET", + internetNetwork); + } catch (InterruptedException e) { + fail("NetworkCallback wait was interrupted."); + } finally { + mCm.unregisterNetworkCallback(callback); + } + } + + /** + * Exercises the requestNetwork with NetworkCallback API with timeout - expected to + * fail. Use WIFI and switch Wi-Fi off. + */ + @AppModeFull(reason = "Cannot get WifiManager in instant app mode") + @Test + public void testRequestNetworkCallback_onUnavailable() { + final boolean previousWifiEnabledState = mWifiManager.isWifiEnabled(); + if (previousWifiEnabledState) { + mCtsNetUtils.ensureWifiDisconnected(null); + } + + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.requestNetwork(new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI) + .build(), callback, 100); + + try { + // Wait to get callback for unavailability of requested network + assertTrue("Did not receive NetworkCallback#onUnavailable", + callback.waitForUnavailable()); + } catch (InterruptedException e) { + fail("NetworkCallback wait was interrupted."); + } finally { + mCm.unregisterNetworkCallback(callback); + if (previousWifiEnabledState) { + mCtsNetUtils.connectToWifi(); + } + } + } + + private InetAddress getFirstV4Address(Network network) { + LinkProperties linkProperties = mCm.getLinkProperties(network); + for (InetAddress address : linkProperties.getAddresses()) { + if (address instanceof Inet4Address) { + return address; + } + } + return null; + } + + /** Verify restricted networks cannot be requested. */ + @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps") + @Test + public void testRestrictedNetworks() { + // Verify we can request unrestricted networks: + NetworkRequest request = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_INTERNET).build(); + NetworkCallback callback = new NetworkCallback(); + mCm.requestNetwork(request, callback); + mCm.unregisterNetworkCallback(callback); + // Verify we cannot request restricted networks: + request = new NetworkRequest.Builder().addCapability(NET_CAPABILITY_IMS).build(); + callback = new NetworkCallback(); + try { + mCm.requestNetwork(request, callback); + fail("No exception thrown when restricted network requested."); + } catch (SecurityException expected) {} + } + + // Returns "true", "false" or "none" + private String getWifiMeteredStatus(String ssid) throws Exception { + // Interestingly giving the SSID as an argument to list wifi-networks + // only works iff the network in question has the "false" policy. + // Also unfortunately runShellCommand does not pass the command to the interpreter + // so it's not possible to | grep the ssid. + final String command = "cmd netpolicy list wifi-networks"; + final String policyString = runShellCommand(mInstrumentation, command); + + final Matcher m = Pattern.compile("^" + ssid + ";(true|false|none)$", + Pattern.MULTILINE | Pattern.UNIX_LINES).matcher(policyString); + if (!m.find()) { + fail("Unexpected format from cmd netpolicy"); + } + return m.group(1); + } + + // metered should be "true", "false" or "none" + private void setWifiMeteredStatus(String ssid, String metered) throws Exception { + final String setCommand = "cmd netpolicy set metered-network " + ssid + " " + metered; + runShellCommand(mInstrumentation, setCommand); + assertEquals(getWifiMeteredStatus(ssid), metered); + } + + private String unquoteSSID(String ssid) { + // SSID is returned surrounded by quotes if it can be decoded as UTF-8. + // Otherwise it's guaranteed not to start with a quote. + if (ssid.charAt(0) == '"') { + return ssid.substring(1, ssid.length() - 1); + } else { + return ssid; + } + } + + private void waitForActiveNetworkMetered(int targetTransportType, boolean requestedMeteredness) + throws Exception { + final CountDownLatch latch = new CountDownLatch(1); + final NetworkCallback networkCallback = new NetworkCallback() { + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) { + if (!nc.hasTransport(targetTransportType)) return; + + final boolean metered = !nc.hasCapability(NET_CAPABILITY_NOT_METERED); + if (metered == requestedMeteredness) { + latch.countDown(); + } + } + }; + // Registering a callback here guarantees onCapabilitiesChanged is called immediately + // with the current setting. Therefore, if the setting has already been changed, + // this method will return right away, and if not it will wait for the setting to change. + mCm.registerDefaultNetworkCallback(networkCallback); + if (!latch.await(NETWORK_CHANGE_METEREDNESS_TIMEOUT, TimeUnit.MILLISECONDS)) { + fail("Timed out waiting for active network metered status to change to " + + requestedMeteredness + " ; network = " + mCm.getActiveNetwork()); + } + mCm.unregisterNetworkCallback(networkCallback); + } + + private void assertMultipathPreferenceIsEventually(Network network, int oldValue, + int expectedValue) { + // Quick check : if oldValue == expectedValue, there is no way to guarantee the test + // is not flaky. + assertNotSame(oldValue, expectedValue); + + for (int i = 0; i < NUM_TRIES_MULTIPATH_PREF_CHECK; ++i) { + final int actualValue = mCm.getMultipathPreference(network); + if (actualValue == expectedValue) { + return; + } + if (actualValue != oldValue) { + fail("Multipath preference is neither previous (" + oldValue + + ") nor expected (" + expectedValue + ")"); + } + SystemClock.sleep(INTERVAL_MULTIPATH_PREF_CHECK_MS); + } + fail("Timed out waiting for multipath preference to change. expected = " + + expectedValue + " ; actual = " + mCm.getMultipathPreference(network)); + } + + private int getCurrentMeteredMultipathPreference(ContentResolver resolver) { + final String rawMeteredPref = Settings.Global.getString(resolver, + NETWORK_METERED_MULTIPATH_PREFERENCE); + return TextUtils.isEmpty(rawMeteredPref) + ? getIntResourceForName(NETWORK_METERED_MULTIPATH_PREFERENCE_RES_NAME) + : Integer.parseInt(rawMeteredPref); + } + + private int findNextPrefValue(ContentResolver resolver) { + // A bit of a nuclear hammer, but race conditions in CTS are bad. To be able to + // detect a correct setting value without race conditions, the next pref must + // be a valid value (range 0..3) that is different from the old setting of the + // metered preference and from the unmetered preference. + final int meteredPref = getCurrentMeteredMultipathPreference(resolver); + final int unmeteredPref = ConnectivityManager.MULTIPATH_PREFERENCE_UNMETERED; + if (0 != meteredPref && 0 != unmeteredPref) return 0; + if (1 != meteredPref && 1 != unmeteredPref) return 1; + return 2; + } + + /** + * Verify that getMultipathPreference does return appropriate values + * for metered and unmetered networks. + */ + @AppModeFull(reason = "Cannot get WifiManager in instant app mode") + @Test + public void testGetMultipathPreference() throws Exception { + final ContentResolver resolver = mContext.getContentResolver(); + ensureWifiConnected(); + final String ssid = unquoteSSID(mWifiManager.getConnectionInfo().getSSID()); + final String oldMeteredSetting = getWifiMeteredStatus(ssid); + final String oldMeteredMultipathPreference = Settings.Global.getString( + resolver, NETWORK_METERED_MULTIPATH_PREFERENCE); + try { + final int initialMeteredPreference = getCurrentMeteredMultipathPreference(resolver); + int newMeteredPreference = findNextPrefValue(resolver); + Settings.Global.putString(resolver, NETWORK_METERED_MULTIPATH_PREFERENCE, + Integer.toString(newMeteredPreference)); + setWifiMeteredStatus(ssid, "true"); + waitForActiveNetworkMetered(TRANSPORT_WIFI, true); + // Wifi meterness changes from unmetered to metered will disconnect and reconnect since + // R. + final Network network = ensureWifiConnected(); + assertEquals(ssid, unquoteSSID(mWifiManager.getConnectionInfo().getSSID())); + assertEquals(mCm.getNetworkCapabilities(network).hasCapability( + NET_CAPABILITY_NOT_METERED), false); + assertMultipathPreferenceIsEventually(network, initialMeteredPreference, + newMeteredPreference); + + final int oldMeteredPreference = newMeteredPreference; + newMeteredPreference = findNextPrefValue(resolver); + Settings.Global.putString(resolver, NETWORK_METERED_MULTIPATH_PREFERENCE, + Integer.toString(newMeteredPreference)); + assertEquals(mCm.getNetworkCapabilities(network).hasCapability( + NET_CAPABILITY_NOT_METERED), false); + assertMultipathPreferenceIsEventually(network, + oldMeteredPreference, newMeteredPreference); + + setWifiMeteredStatus(ssid, "false"); + // No disconnect from unmetered to metered. + waitForActiveNetworkMetered(TRANSPORT_WIFI, false); + assertEquals(mCm.getNetworkCapabilities(network).hasCapability( + NET_CAPABILITY_NOT_METERED), true); + assertMultipathPreferenceIsEventually(network, newMeteredPreference, + ConnectivityManager.MULTIPATH_PREFERENCE_UNMETERED); + } finally { + Settings.Global.putString(resolver, NETWORK_METERED_MULTIPATH_PREFERENCE, + oldMeteredMultipathPreference); + setWifiMeteredStatus(ssid, oldMeteredSetting); + } + } + + // TODO: move the following socket keep alive test to dedicated test class. + /** + * Callback used in tcp keepalive offload that allows caller to wait callback fires. + */ + private static class TestSocketKeepaliveCallback extends SocketKeepalive.Callback { + public enum CallbackType { ON_STARTED, ON_STOPPED, ON_ERROR }; + + public static class CallbackValue { + public final CallbackType callbackType; + public final int error; + + private CallbackValue(final CallbackType type, final int error) { + this.callbackType = type; + this.error = error; + } + + public static class OnStartedCallback extends CallbackValue { + OnStartedCallback() { super(CallbackType.ON_STARTED, 0); } + } + + public static class OnStoppedCallback extends CallbackValue { + OnStoppedCallback() { super(CallbackType.ON_STOPPED, 0); } + } + + public static class OnErrorCallback extends CallbackValue { + OnErrorCallback(final int error) { super(CallbackType.ON_ERROR, error); } + } + + @Override + public boolean equals(Object o) { + return o.getClass() == this.getClass() + && 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.OnStartedCallback()); + } + + @Override + public void onStopped() { + mCallbacks.add(new CallbackValue.OnStoppedCallback()); + } + + @Override + public void onError(final int error) { + mCallbacks.add(new CallbackValue.OnErrorCallback(error)); + } + + public CallbackValue pollCallback() { + try { + return mCallbacks.poll(KEEPALIVE_CALLBACK_TIMEOUT_MS, + TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + fail("Callback not seen after " + KEEPALIVE_CALLBACK_TIMEOUT_MS + " ms"); + } + return null; + } + private void expectCallback(CallbackValue expectedCallback) { + final CallbackValue actualCallback = pollCallback(); + assertEquals(expectedCallback, actualCallback); + } + + public void expectStarted() { + expectCallback(new CallbackValue.OnStartedCallback()); + } + + public void expectStopped() { + expectCallback(new CallbackValue.OnStoppedCallback()); + } + + public void expectError(int error) { + expectCallback(new CallbackValue.OnErrorCallback(error)); + } + } + + private InetAddress getAddrByName(final String hostname, final int family) throws Exception { + final InetAddress[] allAddrs = InetAddress.getAllByName(hostname); + for (InetAddress addr : allAddrs) { + if (family == AF_INET && addr instanceof Inet4Address) return addr; + + if (family == AF_INET6 && addr instanceof Inet6Address) return addr; + + if (family == AF_UNSPEC) return addr; + } + return null; + } + + private Socket getConnectedSocket(final Network network, final String host, final int port, + final int family) throws Exception { + final Socket s = network.getSocketFactory().createSocket(); + try { + final InetAddress addr = getAddrByName(host, family); + if (addr == null) fail("Fail to get destination address for " + family); + + final InetSocketAddress sockAddr = new InetSocketAddress(addr, port); + s.connect(sockAddr); + } catch (Exception e) { + s.close(); + throw e; + } + return s; + } + + private int getSupportedKeepalivesForNet(@NonNull Network network) throws Exception { + final NetworkCapabilities nc = mCm.getNetworkCapabilities(network); + + // Get number of supported concurrent keepalives for testing network. + final int[] keepalivesPerTransport = KeepaliveUtils.getSupportedKeepalives(mContext); + return KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities( + keepalivesPerTransport, nc); + } + + private static boolean isTcpKeepaliveSupportedByKernel() { + final String kVersionString = VintfRuntimeInfo.getKernelVersion(); + return compareMajorMinorVersion(kVersionString, "4.8") >= 0; + } + + private static Pair getVersionFromString(String version) { + // Only gets major and minor number of the version string. + final Pattern versionPattern = Pattern.compile("^(\\d+)(\\.(\\d+))?.*"); + final Matcher m = versionPattern.matcher(version); + if (m.matches()) { + final int major = Integer.parseInt(m.group(1)); + final int minor = TextUtils.isEmpty(m.group(3)) ? 0 : Integer.parseInt(m.group(3)); + return new Pair<>(major, minor); + } else { + return new Pair<>(0, 0); + } + } + + // TODO: Move to util class. + private static int compareMajorMinorVersion(final String s1, final String s2) { + final Pair v1 = getVersionFromString(s1); + final Pair v2 = getVersionFromString(s2); + + if (v1.first == v2.first) { + return Integer.compare(v1.second, v2.second); + } else { + return Integer.compare(v1.first, v2.first); + } + } + + /** + * Verifies that version string compare logic returns expected result for various cases. + * Note that only major and minor number are compared. + */ + @Test + public void testMajorMinorVersionCompare() { + assertEquals(0, compareMajorMinorVersion("4.8.1", "4.8")); + assertEquals(1, compareMajorMinorVersion("4.9", "4.8.1")); + assertEquals(1, compareMajorMinorVersion("5.0", "4.8")); + assertEquals(1, compareMajorMinorVersion("5", "4.8")); + assertEquals(0, compareMajorMinorVersion("5", "5.0")); + assertEquals(1, compareMajorMinorVersion("5-beta1", "4.8")); + assertEquals(0, compareMajorMinorVersion("4.8.0.0", "4.8")); + assertEquals(0, compareMajorMinorVersion("4.8-RC1", "4.8")); + assertEquals(0, compareMajorMinorVersion("4.8", "4.8")); + assertEquals(-1, compareMajorMinorVersion("3.10", "4.8.0")); + assertEquals(-1, compareMajorMinorVersion("4.7.10.10", "4.8")); + } + + /** + * Verifies that the keepalive API cannot create any keepalive when the maximum number of + * keepalives is set to 0. + */ + @AppModeFull(reason = "Cannot get WifiManager in instant app mode") + @Test + public void testKeepaliveWifiUnsupported() throws Exception { + if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) { + Log.i(TAG, "testKeepaliveUnsupported cannot execute unless device" + + " supports WiFi"); + return; + } + + final Network network = ensureWifiConnected(); + if (getSupportedKeepalivesForNet(network) != 0) return; + final InetAddress srcAddr = getFirstV4Address(network); + assumeTrue("This test requires native IPv4", srcAddr != null); + + runWithShellPermissionIdentity(() -> { + assertEquals(0, createConcurrentSocketKeepalives(network, srcAddr, 1, 0)); + assertEquals(0, createConcurrentSocketKeepalives(network, srcAddr, 0, 1)); + }); + } + + @AppModeFull(reason = "Cannot get WifiManager in instant app mode") + @Test + @SkipPresubmit(reason = "Keepalive is not supported on virtual hardware") + public void testCreateTcpKeepalive() throws Exception { + if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) { + Log.i(TAG, "testCreateTcpKeepalive cannot execute unless device supports WiFi"); + return; + } + + final Network network = ensureWifiConnected(); + if (getSupportedKeepalivesForNet(network) == 0) return; + final InetAddress srcAddr = getFirstV4Address(network); + assumeTrue("This test requires native IPv4", srcAddr != null); + + // If kernel < 4.8 then it doesn't support TCP keepalive, but it might still support + // NAT-T keepalive. If keepalive limits from resource overlay is not zero, TCP keepalive + // needs to be supported except if the kernel doesn't support it. + if (!isTcpKeepaliveSupportedByKernel()) { + // Verify that the callback result is expected. + runWithShellPermissionIdentity(() -> { + assertEquals(0, createConcurrentSocketKeepalives(network, srcAddr, 0, 1)); + }); + Log.i(TAG, "testCreateTcpKeepalive is skipped for kernel " + + VintfRuntimeInfo.getKernelVersion()); + return; + } + + final byte[] requestBytes = CtsNetUtils.HTTP_REQUEST.getBytes("UTF-8"); + // So far only ipv4 tcp keepalive offload is supported. + // TODO: add test case for ipv6 tcp keepalive offload when it is supported. + try (Socket s = getConnectedSocket(network, TEST_HOST, HTTP_PORT, AF_INET)) { + + // Should able to start keep alive offload when socket is idle. + final Executor executor = mContext.getMainExecutor(); + final TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback(); + + mUiAutomation.adoptShellPermissionIdentity(); + try (SocketKeepalive sk = mCm.createSocketKeepalive(network, s, executor, callback)) { + sk.start(MIN_KEEPALIVE_INTERVAL); + callback.expectStarted(); + + // App should not able to write during keepalive offload. + final OutputStream out = s.getOutputStream(); + try { + out.write(requestBytes); + fail("Should not able to write"); + } catch (IOException e) { } + // App should not able to read during keepalive offload. + final InputStream in = s.getInputStream(); + byte[] responseBytes = new byte[4096]; + try { + in.read(responseBytes); + fail("Should not able to read"); + } catch (IOException e) { } + + // Stop. + sk.stop(); + callback.expectStopped(); + } finally { + mUiAutomation.dropShellPermissionIdentity(); + } + + // Ensure socket is still connected. + assertTrue(s.isConnected()); + assertFalse(s.isClosed()); + + // Let socket be not idle. + try { + final OutputStream out = s.getOutputStream(); + out.write(requestBytes); + } catch (IOException e) { + fail("Failed to write data " + e); + } + // Make sure response data arrives. + final MessageQueue fdHandlerQueue = Looper.getMainLooper().getQueue(); + final FileDescriptor fd = s.getFileDescriptor$(); + final CountDownLatch mOnReceiveLatch = new CountDownLatch(1); + fdHandlerQueue.addOnFileDescriptorEventListener(fd, EVENT_INPUT, (readyFd, events) -> { + mOnReceiveLatch.countDown(); + return 0; // Unregister listener. + }); + if (!mOnReceiveLatch.await(2, TimeUnit.SECONDS)) { + fdHandlerQueue.removeOnFileDescriptorEventListener(fd); + fail("Timeout: no response data"); + } + + // Should get ERROR_SOCKET_NOT_IDLE because there is still data in the receive queue + // that has not been read. + mUiAutomation.adoptShellPermissionIdentity(); + try (SocketKeepalive sk = mCm.createSocketKeepalive(network, s, executor, callback)) { + sk.start(MIN_KEEPALIVE_INTERVAL); + callback.expectError(SocketKeepalive.ERROR_SOCKET_NOT_IDLE); + } finally { + mUiAutomation.dropShellPermissionIdentity(); + } + } + } + + private ArrayList createConcurrentKeepalivesOfType( + int requestCount, @NonNull TestSocketKeepaliveCallback callback, + Supplier kaFactory) { + final ArrayList kalist = new ArrayList<>(); + + int remainingRetries = MAX_KEEPALIVE_RETRY_COUNT; + + // Test concurrent keepalives with the given supplier. + while (kalist.size() < requestCount) { + final SocketKeepalive ka = kaFactory.get(); + ka.start(MIN_KEEPALIVE_INTERVAL); + TestSocketKeepaliveCallback.CallbackValue cv = callback.pollCallback(); + assertNotNull(cv); + if (cv.callbackType == TestSocketKeepaliveCallback.CallbackType.ON_ERROR) { + if (kalist.size() == 0 && cv.error == SocketKeepalive.ERROR_UNSUPPORTED) { + // Unsupported. + break; + } else if (cv.error == SocketKeepalive.ERROR_INSUFFICIENT_RESOURCES) { + // Limit reached or temporary unavailable due to stopped slot is not yet + // released. + if (remainingRetries > 0) { + SystemClock.sleep(INTERVAL_KEEPALIVE_RETRY_MS); + remainingRetries--; + continue; + } + break; + } + } + if (cv.callbackType == TestSocketKeepaliveCallback.CallbackType.ON_STARTED) { + kalist.add(ka); + } else { + fail("Unexpected error when creating " + (kalist.size() + 1) + " " + + ka.getClass().getSimpleName() + ": " + cv); + } + } + + return kalist; + } + + private @NonNull ArrayList createConcurrentNattSocketKeepalives( + @NonNull Network network, @NonNull InetAddress srcAddr, int requestCount, + @NonNull TestSocketKeepaliveCallback callback) throws Exception { + + final Executor executor = mContext.getMainExecutor(); + + // Initialize a real NaT-T socket. + final IpSecManager mIpSec = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE); + final UdpEncapsulationSocket nattSocket = mIpSec.openUdpEncapsulationSocket(); + final InetAddress dstAddr = getAddrByName(TEST_HOST, AF_INET); + assertNotNull(srcAddr); + assertNotNull(dstAddr); + + // Test concurrent Nat-T keepalives. + final ArrayList result = createConcurrentKeepalivesOfType(requestCount, + callback, () -> mCm.createSocketKeepalive(network, nattSocket, + srcAddr, dstAddr, executor, callback)); + + nattSocket.close(); + return result; + } + + private @NonNull ArrayList createConcurrentTcpSocketKeepalives( + @NonNull Network network, int requestCount, + @NonNull TestSocketKeepaliveCallback callback) { + final Executor executor = mContext.getMainExecutor(); + + // Create concurrent TCP keepalives. + return createConcurrentKeepalivesOfType(requestCount, callback, () -> { + // Assert that TCP connections can be established. The file descriptor of tcp + // sockets will be duplicated and kept valid in service side if the keepalives are + // successfully started. + try (Socket tcpSocket = getConnectedSocket(network, TEST_HOST, HTTP_PORT, + AF_INET)) { + return mCm.createSocketKeepalive(network, tcpSocket, executor, callback); + } catch (Exception e) { + fail("Unexpected error when creating TCP socket: " + e); + } + return null; + }); + } + + /** + * Creates concurrent keepalives until the specified counts of each type of keepalives are + * reached or the expected error callbacks are received for each type of keepalives. + * + * @return the total number of keepalives created. + */ + private int createConcurrentSocketKeepalives( + @NonNull Network network, @NonNull InetAddress srcAddr, int nattCount, int tcpCount) + throws Exception { + final ArrayList kalist = new ArrayList<>(); + final TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback(); + + kalist.addAll(createConcurrentNattSocketKeepalives(network, srcAddr, nattCount, callback)); + kalist.addAll(createConcurrentTcpSocketKeepalives(network, tcpCount, callback)); + + final int ret = kalist.size(); + + // Clean up. + for (final SocketKeepalive ka : kalist) { + ka.stop(); + callback.expectStopped(); + } + kalist.clear(); + + return ret; + } + + /** + * Verifies that the concurrent keepalive slots meet the minimum requirement, and don't + * get leaked after iterations. + */ + @AppModeFull(reason = "Cannot get WifiManager in instant app mode") + @Test + @SkipPresubmit(reason = "Keepalive is not supported on virtual hardware") + public void testSocketKeepaliveLimitWifi() throws Exception { + if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) { + Log.i(TAG, "testSocketKeepaliveLimitWifi cannot execute unless device" + + " supports WiFi"); + return; + } + + final Network network = ensureWifiConnected(); + final int supported = getSupportedKeepalivesForNet(network); + if (supported == 0) { + return; + } + final InetAddress srcAddr = getFirstV4Address(network); + assumeTrue("This test requires native IPv4", srcAddr != null); + + runWithShellPermissionIdentity(() -> { + // Verifies that the supported keepalive slots meet MIN_SUPPORTED_KEEPALIVE_COUNT. + assertGreaterOrEqual(supported, MIN_SUPPORTED_WIFI_KEEPALIVE_COUNT); + + // Verifies that Nat-T keepalives can be established. + assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, + supported + 1, 0)); + // Verifies that keepalives don't get leaked in second round. + assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, supported, + 0)); + }); + + // If kernel < 4.8 then it doesn't support TCP keepalive, but it might still support + // NAT-T keepalive. Test below cases only if TCP keepalive is supported by kernel. + if (!isTcpKeepaliveSupportedByKernel()) return; + + runWithShellPermissionIdentity(() -> { + assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, 0, + supported + 1)); + + // Verifies that different types can be established at the same time. + assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, + supported / 2, supported - supported / 2)); + + // Verifies that keepalives don't get leaked in second round. + assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, 0, + supported)); + assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, + supported / 2, supported - supported / 2)); + }); + } + + /** + * Verifies that the concurrent keepalive slots meet the minimum telephony requirement, and + * don't get leaked after iterations. + */ + @AppModeFull(reason = "Cannot request network in instant app mode") + @Test + @SkipPresubmit(reason = "Keepalive is not supported on virtual hardware") + public void testSocketKeepaliveLimitTelephony() throws Exception { + if (!mPackageManager.hasSystemFeature(FEATURE_TELEPHONY)) { + Log.i(TAG, "testSocketKeepaliveLimitTelephony cannot execute unless device" + + " supports telephony"); + return; + } + + final int firstSdk = Build.VERSION.FIRST_SDK_INT; + if (firstSdk < Build.VERSION_CODES.Q) { + Log.i(TAG, "testSocketKeepaliveLimitTelephony: skip test for devices launching" + + " before Q: " + firstSdk); + return; + } + + final Network network = mCtsNetUtils.connectToCell(); + final int supported = getSupportedKeepalivesForNet(network); + final InetAddress srcAddr = getFirstV4Address(network); + assumeTrue("This test requires native IPv4", srcAddr != null); + + runWithShellPermissionIdentity(() -> { + // Verifies that the supported keepalive slots meet minimum requirement. + assertGreaterOrEqual(supported, MIN_SUPPORTED_CELLULAR_KEEPALIVE_COUNT); + // Verifies that Nat-T keepalives can be established. + assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, + supported + 1, 0)); + // Verifies that keepalives don't get leaked in second round. + assertEquals(supported, createConcurrentSocketKeepalives(network, srcAddr, supported, + 0)); + }); + } + + private int getIntResourceForName(@NonNull String resName) { + final Resources r = mContext.getResources(); + final int resId = r.getIdentifier(resName, "integer", "android"); + return r.getInteger(resId); + } + + /** + * Verifies that the keepalive slots are limited as customized for unprivileged requests. + */ + @AppModeFull(reason = "Cannot get WifiManager in instant app mode") + @Test + @SkipPresubmit(reason = "Keepalive is not supported on virtual hardware") + public void testSocketKeepaliveUnprivileged() throws Exception { + if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) { + Log.i(TAG, "testSocketKeepaliveUnprivileged cannot execute unless device" + + " supports WiFi"); + return; + } + + final Network network = ensureWifiConnected(); + final int supported = getSupportedKeepalivesForNet(network); + if (supported == 0) { + return; + } + final InetAddress srcAddr = getFirstV4Address(network); + assumeTrue("This test requires native IPv4", srcAddr != null); + + // Resource ID might be shifted on devices that compiled with different symbols. + // Thus, resolve ID at runtime is needed. + final int allowedUnprivilegedPerUid = + getIntResourceForName(KEEPALIVE_ALLOWED_UNPRIVILEGED_RES_NAME); + final int reservedPrivilegedSlots = + getIntResourceForName(KEEPALIVE_RESERVED_PER_SLOT_RES_NAME); + // Verifies that unprivileged request per uid cannot exceed the limit customized in the + // resource. Currently, unprivileged keepalive slots are limited to Nat-T only, this test + // does not apply to TCP. + assertGreaterOrEqual(supported, reservedPrivilegedSlots); + assertGreaterOrEqual(supported, allowedUnprivilegedPerUid); + final int expectedUnprivileged = + Math.min(allowedUnprivilegedPerUid, supported - reservedPrivilegedSlots); + assertEquals(expectedUnprivileged, + createConcurrentSocketKeepalives(network, srcAddr, supported + 1, 0)); + } + + private static void assertGreaterOrEqual(long greater, long lesser) { + assertTrue("" + greater + " expected to be greater than or equal to " + lesser, + greater >= lesser); + } + + /** + * Verifies that apps are not allowed to access restricted networks even if they declare the + * CONNECTIVITY_USE_RESTRICTED_NETWORKS permission in their manifests. + * See. b/144679405. + */ + @AppModeFull(reason = "Cannot get WifiManager in instant app mode") + @Test + public void testRestrictedNetworkPermission() throws Exception { + // Ensure that CONNECTIVITY_USE_RESTRICTED_NETWORKS isn't granted to this package. + final PackageInfo app = mPackageManager.getPackageInfo(mContext.getPackageName(), + GET_PERMISSIONS); + final int index = ArrayUtils.indexOf( + app.requestedPermissions, CONNECTIVITY_USE_RESTRICTED_NETWORKS); + assertTrue(index >= 0); + assertTrue(app.requestedPermissionsFlags[index] != PERMISSION_GRANTED); + + // Ensure that NetworkUtils.queryUserAccess always returns false since this package should + // not have netd system permission to call this function. + final Network wifiNetwork = ensureWifiConnected(); + assertFalse(NetworkUtils.queryUserAccess(Binder.getCallingUid(), wifiNetwork.netId)); + + // Ensure that this package cannot bind to any restricted network that's currently + // connected. + Network[] networks = mCm.getAllNetworks(); + for (Network network : networks) { + NetworkCapabilities nc = mCm.getNetworkCapabilities(network); + if (nc != null && !nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) { + try { + network.bindSocket(new Socket()); + fail("Bind to restricted network " + network + " unexpectedly succeeded"); + } catch (IOException expected) {} + } + } + } + + /** + * Verifies that apps are allowed to call setAirplaneMode if they declare + * NETWORK_AIRPLANE_MODE permission in their manifests. + * See b/145164696. + */ + @AppModeFull(reason = "NETWORK_AIRPLANE_MODE permission can't be granted to instant apps") + @Test + public void testSetAirplaneMode() throws Exception{ + final boolean supportWifi = mPackageManager.hasSystemFeature(FEATURE_WIFI); + final boolean supportTelephony = mPackageManager.hasSystemFeature(FEATURE_TELEPHONY); + // store the current state of airplane mode + final boolean isAirplaneModeEnabled = isAirplaneModeEnabled(); + final TestableNetworkCallback wifiCb = new TestableNetworkCallback(); + final TestableNetworkCallback telephonyCb = new TestableNetworkCallback(); + // disable airplane mode to reach a known state + runShellCommand("cmd connectivity airplane-mode disable"); + // Verify that networks are available as expected if wifi or cell is supported. Continue the + // test if none of them are supported since test should still able to verify the permission + // mechanism. + if (supportWifi) requestAndWaitForAvailable(makeWifiNetworkRequest(), wifiCb); + if (supportTelephony) requestAndWaitForAvailable(makeCellNetworkRequest(), telephonyCb); + + try { + // Verify we cannot set Airplane Mode without correct permission: + try { + setAndVerifyAirplaneMode(true); + fail("SecurityException should have been thrown when setAirplaneMode was called" + + "without holding permission NETWORK_AIRPLANE_MODE."); + } catch (SecurityException expected) {} + + // disable airplane mode again to reach a known state + runShellCommand("cmd connectivity airplane-mode disable"); + + // adopt shell permission which holds NETWORK_AIRPLANE_MODE + mUiAutomation.adoptShellPermissionIdentity(); + + // Verify we can enable Airplane Mode with correct permission: + try { + setAndVerifyAirplaneMode(true); + } catch (SecurityException e) { + fail("SecurityException should not have been thrown when setAirplaneMode(true) was" + + "called whilst holding the NETWORK_AIRPLANE_MODE permission."); + } + // Verify that the enabling airplane mode takes effect as expected to prevent flakiness + // caused by fast airplane mode switches. Ensure network lost before turning off + // airplane mode. + if (supportWifi) waitForLost(wifiCb); + if (supportTelephony) waitForLost(telephonyCb); + + // Verify we can disable Airplane Mode with correct permission: + try { + setAndVerifyAirplaneMode(false); + } catch (SecurityException e) { + fail("SecurityException should not have been thrown when setAirplaneMode(false) was" + + "called whilst holding the NETWORK_AIRPLANE_MODE permission."); + } + // Verify that turning airplane mode off takes effect as expected. + if (supportWifi) waitForAvailable(wifiCb); + if (supportTelephony) waitForAvailable(telephonyCb); + } finally { + if (supportWifi) mCm.unregisterNetworkCallback(wifiCb); + if (supportTelephony) mCm.unregisterNetworkCallback(telephonyCb); + // Restore the previous state of airplane mode and permissions: + runShellCommand("cmd connectivity airplane-mode " + + (isAirplaneModeEnabled ? "enable" : "disable")); + mUiAutomation.dropShellPermissionIdentity(); + } + } + + private void requestAndWaitForAvailable(@NonNull final NetworkRequest request, + @NonNull final TestableNetworkCallback cb) { + mCm.registerNetworkCallback(request, cb); + waitForAvailable(cb); + } + + private void waitForAvailable(@NonNull final TestableNetworkCallback cb) { + cb.eventuallyExpect(CallbackEntry.AVAILABLE, AIRPLANE_MODE_CHANGE_TIMEOUT_MS, + c -> c instanceof CallbackEntry.Available); + } + + private void waitForLost(@NonNull final TestableNetworkCallback cb) { + cb.eventuallyExpect(CallbackEntry.LOST, AIRPLANE_MODE_CHANGE_TIMEOUT_MS, + c -> c instanceof CallbackEntry.Lost); + } + + private void setAndVerifyAirplaneMode(Boolean expectedResult) + throws Exception { + final CompletableFuture actualResult = new CompletableFuture(); + BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // The defaultValue of getExtraBoolean should be the opposite of what is + // expected, thus ensuring a test failure if the extra is absent. + actualResult.complete(intent.getBooleanExtra("state", !expectedResult)); + } + }; + try { + mContext.registerReceiver(receiver, + new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED)); + mCm.setAirplaneMode(expectedResult); + final String msg = "Setting Airplane Mode failed,"; + assertEquals(msg, expectedResult, actualResult.get(AIRPLANE_MODE_CHANGE_TIMEOUT_MS, + TimeUnit.MILLISECONDS)); + } finally { + mContext.unregisterReceiver(receiver); + } + } + + private static boolean isAirplaneModeEnabled() { + return runShellCommand("cmd connectivity airplane-mode") + .trim().equals("enabled"); + } + + @Test + public void testGetCaptivePortalServerUrl() { + final String url = runAsShell(NETWORK_SETTINGS, mCm::getCaptivePortalServerUrl); + assertNotNull("getCaptivePortalServerUrl must not be null", url); + try { + final URL parsedUrl = new URL(url); + // As per the javadoc, the URL must be HTTP + assertEquals("Invalid captive portal URL protocol", "http", parsedUrl.getProtocol()); + } catch (MalformedURLException e) { + throw new AssertionFailedError("Captive portal server URL is invalid: " + e); + } + } +} diff --git a/tests/cts/net/src/android/net/cts/CredentialsTest.java b/tests/cts/net/src/android/net/cts/CredentialsTest.java new file mode 100644 index 0000000000..91c3621eab --- /dev/null +++ b/tests/cts/net/src/android/net/cts/CredentialsTest.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2008 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.cts; + +import android.net.Credentials; +import android.test.AndroidTestCase; + +public class CredentialsTest extends AndroidTestCase { + + public void testCredentials() { + // new the Credentials instance + // Test with zero inputs + Credentials cred = new Credentials(0, 0, 0); + assertEquals(0, cred.getGid()); + assertEquals(0, cred.getPid()); + assertEquals(0, cred.getUid()); + + // Test with big integer + cred = new Credentials(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); + assertEquals(Integer.MAX_VALUE, cred.getGid()); + assertEquals(Integer.MAX_VALUE, cred.getPid()); + assertEquals(Integer.MAX_VALUE, cred.getUid()); + + // Test with big negative integer + cred = new Credentials(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); + assertEquals(Integer.MIN_VALUE, cred.getGid()); + assertEquals(Integer.MIN_VALUE, cred.getPid()); + assertEquals(Integer.MIN_VALUE, cred.getUid()); + } +} diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java new file mode 100644 index 0000000000..4acbbcfbdd --- /dev/null +++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java @@ -0,0 +1,756 @@ +/* + * 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.cts; + +import static android.net.DnsResolver.CLASS_IN; +import static android.net.DnsResolver.FLAG_EMPTY; +import static android.net.DnsResolver.FLAG_NO_CACHE_LOOKUP; +import static android.net.DnsResolver.TYPE_A; +import static android.net.DnsResolver.TYPE_AAAA; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.system.OsConstants.ETIMEDOUT; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.ContentResolver; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.DnsResolver; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.ParseException; +import android.net.cts.util.CtsNetUtils; +import android.os.CancellationSignal; +import android.os.Handler; +import android.os.Looper; +import android.platform.test.annotations.AppModeFull; +import android.provider.Settings; +import android.system.ErrnoException; +import android.test.AndroidTestCase; +import android.util.Log; + +import com.android.net.module.util.DnsPacket; +import com.android.testutils.SkipPresubmit; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +@AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps") +public class DnsResolverTest extends AndroidTestCase { + private static final String TAG = "DnsResolverTest"; + private static final char[] HEX_CHARS = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + + static final String TEST_DOMAIN = "www.google.com"; + static final String TEST_NX_DOMAIN = "test1-nx.metric.gstatic.com"; + static final String INVALID_PRIVATE_DNS_SERVER = "invalid.google"; + static final String GOOGLE_PRIVATE_DNS_SERVER = "dns.google"; + static final byte[] TEST_BLOB = new byte[]{ + /* Header */ + 0x55, 0x66, /* Transaction ID */ + 0x01, 0x00, /* Flags */ + 0x00, 0x01, /* Questions */ + 0x00, 0x00, /* Answer RRs */ + 0x00, 0x00, /* Authority RRs */ + 0x00, 0x00, /* Additional RRs */ + /* Queries */ + 0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6c, 0x65, + 0x03, 0x63, 0x6f, 0x6d, 0x00, /* Name */ + 0x00, 0x01, /* Type */ + 0x00, 0x01 /* Class */ + }; + static final int TIMEOUT_MS = 12_000; + static final int CANCEL_TIMEOUT_MS = 3_000; + static final int CANCEL_RETRY_TIMES = 5; + static final int QUERY_TIMES = 10; + static final int NXDOMAIN = 3; + + private ContentResolver mCR; + private ConnectivityManager mCM; + private CtsNetUtils mCtsNetUtils; + private Executor mExecutor; + private Executor mExecutorInline; + private DnsResolver mDns; + + private String mOldMode; + private String mOldDnsSpecifier; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mCM = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE); + mDns = DnsResolver.getInstance(); + mExecutor = new Handler(Looper.getMainLooper())::post; + mExecutorInline = (Runnable r) -> r.run(); + mCR = getContext().getContentResolver(); + mCtsNetUtils = new CtsNetUtils(getContext()); + mCtsNetUtils.storePrivateDnsSetting(); + } + + @Override + protected void tearDown() throws Exception { + mCtsNetUtils.restorePrivateDnsSetting(); + super.tearDown(); + } + + private static String byteArrayToHexString(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int i = 0; i < bytes.length; ++i) { + int b = bytes[i] & 0xFF; + hexChars[i * 2] = HEX_CHARS[b >>> 4]; + hexChars[i * 2 + 1] = HEX_CHARS[b & 0x0F]; + } + return new String(hexChars); + } + + private Network[] getTestableNetworks() { + final ArrayList testableNetworks = new ArrayList(); + for (Network network : mCM.getAllNetworks()) { + final NetworkCapabilities nc = mCM.getNetworkCapabilities(network); + if (nc != null + && nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) + && nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { + testableNetworks.add(network); + } + } + + assertTrue( + "This test requires that at least one network be connected. " + + "Please ensure that the device is connected to a network.", + testableNetworks.size() >= 1); + // In order to test query with null network, add null as an element. + // Test cases which query with null network will go on default network. + testableNetworks.add(null); + return testableNetworks.toArray(new Network[0]); + } + + static private void assertGreaterThan(String msg, int first, int second) { + assertTrue(msg + " Excepted " + first + " to be greater than " + second, first > second); + } + + private static class DnsParseException extends Exception { + public DnsParseException(String msg) { + super(msg); + } + } + + private static class DnsAnswer extends DnsPacket { + DnsAnswer(@NonNull byte[] data) throws DnsParseException { + super(data); + + // Check QR field.(query (0), or a response (1)). + if ((mHeader.flags & (1 << 15)) == 0) { + throw new DnsParseException("Not an answer packet"); + } + } + + int getRcode() { + return mHeader.rcode; + } + + int getANCount() { + return mHeader.getRecordCount(ANSECTION); + } + + int getQDCount() { + return mHeader.getRecordCount(QDSECTION); + } + } + + /** + * A query callback that ensures that the query is cancelled and that onAnswer is never + * called. If the query succeeds before it is cancelled, needRetry will return true so the + * test can retry. + */ + class VerifyCancelCallback implements DnsResolver.Callback { + private final CountDownLatch mLatch = new CountDownLatch(1); + private final String mMsg; + private final CancellationSignal mCancelSignal; + private int mRcode; + private DnsAnswer mDnsAnswer; + private String mErrorMsg = null; + + VerifyCancelCallback(@NonNull String msg, @Nullable CancellationSignal cancel) { + mMsg = msg; + mCancelSignal = cancel; + } + + VerifyCancelCallback(@NonNull String msg) { + this(msg, null); + } + + public boolean waitForAnswer(int timeout) throws InterruptedException { + return mLatch.await(timeout, TimeUnit.MILLISECONDS); + } + + public boolean waitForAnswer() throws InterruptedException { + return waitForAnswer(TIMEOUT_MS); + } + + public boolean needRetry() throws InterruptedException { + return mLatch.await(CANCEL_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } + + @Override + public void onAnswer(@NonNull byte[] answer, int rcode) { + if (mCancelSignal != null && mCancelSignal.isCanceled()) { + mErrorMsg = mMsg + " should not have returned any answers"; + mLatch.countDown(); + return; + } + + mRcode = rcode; + try { + mDnsAnswer = new DnsAnswer(answer); + } catch (ParseException | DnsParseException e) { + mErrorMsg = mMsg + e.getMessage(); + mLatch.countDown(); + return; + } + Log.d(TAG, "Reported blob: " + byteArrayToHexString(answer)); + mLatch.countDown(); + } + + @Override + public void onError(@NonNull DnsResolver.DnsException error) { + mErrorMsg = mMsg + error.getMessage(); + mLatch.countDown(); + } + + private void assertValidAnswer() { + assertNull(mErrorMsg); + assertNotNull(mMsg + " No valid answer", mDnsAnswer); + assertEquals(mMsg + " Unexpected error: reported rcode" + mRcode + + " blob's rcode " + mDnsAnswer.getRcode(), mRcode, mDnsAnswer.getRcode()); + } + + public void assertHasAnswer() { + assertValidAnswer(); + // Check rcode field.(0, No error condition). + assertEquals(mMsg + " Response error, rcode: " + mRcode, mRcode, 0); + // Check answer counts. + assertGreaterThan(mMsg + " No answer found", mDnsAnswer.getANCount(), 0); + // Check question counts. + assertGreaterThan(mMsg + " No question found", mDnsAnswer.getQDCount(), 0); + } + + public void assertNXDomain() { + assertValidAnswer(); + // Check rcode field.(3, NXDomain). + assertEquals(mMsg + " Unexpected rcode: " + mRcode, mRcode, NXDOMAIN); + // Check answer counts. Expect 0 answer. + assertEquals(mMsg + " Not an empty answer", mDnsAnswer.getANCount(), 0); + // Check question counts. + assertGreaterThan(mMsg + " No question found", mDnsAnswer.getQDCount(), 0); + } + + public void assertEmptyAnswer() { + assertValidAnswer(); + // Check rcode field.(0, No error condition). + assertEquals(mMsg + " Response error, rcode: " + mRcode, mRcode, 0); + // Check answer counts. Expect 0 answer. + assertEquals(mMsg + " Not an empty answer", mDnsAnswer.getANCount(), 0); + // Check question counts. + assertGreaterThan(mMsg + " No question found", mDnsAnswer.getQDCount(), 0); + } + } + + public void testRawQuery() throws Exception { + doTestRawQuery(mExecutor); + } + + public void testRawQueryInline() throws Exception { + doTestRawQuery(mExecutorInline); + } + + public void testRawQueryBlob() throws Exception { + doTestRawQueryBlob(mExecutor); + } + + public void testRawQueryBlobInline() throws Exception { + doTestRawQueryBlob(mExecutorInline); + } + + public void testRawQueryRoot() throws Exception { + doTestRawQueryRoot(mExecutor); + } + + public void testRawQueryRootInline() throws Exception { + doTestRawQueryRoot(mExecutorInline); + } + + public void testRawQueryNXDomain() throws Exception { + doTestRawQueryNXDomain(mExecutor); + } + + public void testRawQueryNXDomainInline() throws Exception { + doTestRawQueryNXDomain(mExecutorInline); + } + + public void testRawQueryNXDomainWithPrivateDns() throws Exception { + doTestRawQueryNXDomainWithPrivateDns(mExecutor); + } + + public void testRawQueryNXDomainInlineWithPrivateDns() throws Exception { + doTestRawQueryNXDomainWithPrivateDns(mExecutorInline); + } + + public void doTestRawQuery(Executor executor) throws InterruptedException { + final String msg = "RawQuery " + TEST_DOMAIN; + for (Network network : getTestableNetworks()) { + final VerifyCancelCallback callback = new VerifyCancelCallback(msg); + mDns.rawQuery(network, TEST_DOMAIN, CLASS_IN, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP, + executor, null, callback); + + assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.", + callback.waitForAnswer()); + callback.assertHasAnswer(); + } + } + + public void doTestRawQueryBlob(Executor executor) throws InterruptedException { + final byte[] blob = new byte[]{ + /* Header */ + 0x55, 0x66, /* Transaction ID */ + 0x01, 0x00, /* Flags */ + 0x00, 0x01, /* Questions */ + 0x00, 0x00, /* Answer RRs */ + 0x00, 0x00, /* Authority RRs */ + 0x00, 0x00, /* Additional RRs */ + /* Queries */ + 0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6c, 0x65, + 0x03, 0x63, 0x6f, 0x6d, 0x00, /* Name */ + 0x00, 0x01, /* Type */ + 0x00, 0x01 /* Class */ + }; + final String msg = "RawQuery blob " + byteArrayToHexString(blob); + for (Network network : getTestableNetworks()) { + final VerifyCancelCallback callback = new VerifyCancelCallback(msg); + mDns.rawQuery(network, blob, FLAG_NO_CACHE_LOOKUP, executor, null, callback); + + assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.", + callback.waitForAnswer()); + callback.assertHasAnswer(); + } + } + + public void doTestRawQueryRoot(Executor executor) throws InterruptedException { + final String dname = ""; + final String msg = "RawQuery empty dname(ROOT) "; + for (Network network : getTestableNetworks()) { + final VerifyCancelCallback callback = new VerifyCancelCallback(msg); + mDns.rawQuery(network, dname, CLASS_IN, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP, + executor, null, callback); + + assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.", + callback.waitForAnswer()); + // Except no answer record because the root does not have AAAA records. + callback.assertEmptyAnswer(); + } + } + + public void doTestRawQueryNXDomain(Executor executor) throws InterruptedException { + final String msg = "RawQuery " + TEST_NX_DOMAIN; + + for (Network network : getTestableNetworks()) { + final NetworkCapabilities nc = (network != null) + ? mCM.getNetworkCapabilities(network) + : mCM.getNetworkCapabilities(mCM.getActiveNetwork()); + assertNotNull("Couldn't determine NetworkCapabilities for " + network, nc); + // Some cellular networks configure their DNS servers never to return NXDOMAIN, so don't + // test NXDOMAIN on these DNS servers. + // b/144521720 + if (nc.hasTransport(TRANSPORT_CELLULAR)) continue; + final VerifyCancelCallback callback = new VerifyCancelCallback(msg); + mDns.rawQuery(network, TEST_NX_DOMAIN, CLASS_IN, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP, + executor, null, callback); + + assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.", + callback.waitForAnswer()); + callback.assertNXDomain(); + } + } + + public void doTestRawQueryNXDomainWithPrivateDns(Executor executor) + throws InterruptedException { + final String msg = "RawQuery " + TEST_NX_DOMAIN + " with private DNS"; + // Enable private DNS strict mode and set server to dns.google before doing NxDomain test. + // b/144521720 + mCtsNetUtils.setPrivateDnsStrictMode(GOOGLE_PRIVATE_DNS_SERVER); + for (Network network : getTestableNetworks()) { + final Network networkForPrivateDns = + (network != null) ? network : mCM.getActiveNetwork(); + assertNotNull("Can't find network to await private DNS on", networkForPrivateDns); + mCtsNetUtils.awaitPrivateDnsSetting(msg + " wait private DNS setting timeout", + networkForPrivateDns, GOOGLE_PRIVATE_DNS_SERVER, true); + final VerifyCancelCallback callback = new VerifyCancelCallback(msg); + mDns.rawQuery(network, TEST_NX_DOMAIN, CLASS_IN, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP, + executor, null, callback); + + assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.", + callback.waitForAnswer()); + callback.assertNXDomain(); + } + } + + public void testRawQueryCancel() throws InterruptedException { + final String msg = "Test cancel RawQuery " + TEST_DOMAIN; + // Start a DNS query and the cancel it immediately. Use VerifyCancelCallback to expect + // that the query is cancelled before it succeeds. If it is not cancelled before it + // succeeds, retry the test until it is. + for (Network network : getTestableNetworks()) { + boolean retry = false; + int round = 0; + do { + if (++round > CANCEL_RETRY_TIMES) { + fail(msg + " cancel failed " + CANCEL_RETRY_TIMES + " times"); + } + final CountDownLatch latch = new CountDownLatch(1); + final CancellationSignal cancelSignal = new CancellationSignal(); + final VerifyCancelCallback callback = new VerifyCancelCallback(msg, cancelSignal); + mDns.rawQuery(network, TEST_DOMAIN, CLASS_IN, TYPE_AAAA, FLAG_EMPTY, + mExecutor, cancelSignal, callback); + mExecutor.execute(() -> { + cancelSignal.cancel(); + latch.countDown(); + }); + + retry = callback.needRetry(); + assertTrue(msg + " query was not cancelled", + latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + } while (retry); + } + } + + public void testRawQueryBlobCancel() throws InterruptedException { + final String msg = "Test cancel RawQuery blob " + byteArrayToHexString(TEST_BLOB); + // Start a DNS query and the cancel it immediately. Use VerifyCancelCallback to expect + // that the query is cancelled before it succeeds. If it is not cancelled before it + // succeeds, retry the test until it is. + for (Network network : getTestableNetworks()) { + boolean retry = false; + int round = 0; + do { + if (++round > CANCEL_RETRY_TIMES) { + fail(msg + " cancel failed " + CANCEL_RETRY_TIMES + " times"); + } + final CountDownLatch latch = new CountDownLatch(1); + final CancellationSignal cancelSignal = new CancellationSignal(); + final VerifyCancelCallback callback = new VerifyCancelCallback(msg, cancelSignal); + mDns.rawQuery(network, TEST_BLOB, FLAG_EMPTY, mExecutor, cancelSignal, callback); + mExecutor.execute(() -> { + cancelSignal.cancel(); + latch.countDown(); + }); + + retry = callback.needRetry(); + assertTrue(msg + " cancel is not done", + latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + } while (retry); + } + } + + public void testCancelBeforeQuery() throws InterruptedException { + final String msg = "Test cancelled RawQuery " + TEST_DOMAIN; + for (Network network : getTestableNetworks()) { + final VerifyCancelCallback callback = new VerifyCancelCallback(msg); + final CancellationSignal cancelSignal = new CancellationSignal(); + cancelSignal.cancel(); + mDns.rawQuery(network, TEST_DOMAIN, CLASS_IN, TYPE_AAAA, FLAG_EMPTY, + mExecutor, cancelSignal, callback); + + assertTrue(msg + " should not return any answers", + !callback.waitForAnswer(CANCEL_TIMEOUT_MS)); + } + } + + /** + * A query callback for InetAddress that ensures that the query is + * cancelled and that onAnswer is never called. If the query succeeds + * before it is cancelled, needRetry will return true so the + * test can retry. + */ + class VerifyCancelInetAddressCallback implements DnsResolver.Callback> { + private final CountDownLatch mLatch = new CountDownLatch(1); + private final String mMsg; + private final List mAnswers; + private final CancellationSignal mCancelSignal; + private String mErrorMsg = null; + + VerifyCancelInetAddressCallback(@NonNull String msg, @Nullable CancellationSignal cancel) { + this.mMsg = msg; + this.mCancelSignal = cancel; + mAnswers = new ArrayList<>(); + } + + public boolean waitForAnswer() throws InterruptedException { + return mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS); + } + + public boolean needRetry() throws InterruptedException { + return mLatch.await(CANCEL_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } + + public boolean isAnswerEmpty() { + return mAnswers.isEmpty(); + } + + public boolean hasIpv6Answer() { + for (InetAddress answer : mAnswers) { + if (answer instanceof Inet6Address) return true; + } + return false; + } + + public boolean hasIpv4Answer() { + for (InetAddress answer : mAnswers) { + if (answer instanceof Inet4Address) return true; + } + return false; + } + + public void assertNoError() { + assertNull(mErrorMsg); + } + + @Override + public void onAnswer(@NonNull List answerList, int rcode) { + if (mCancelSignal != null && mCancelSignal.isCanceled()) { + mErrorMsg = mMsg + " should not have returned any answers"; + mLatch.countDown(); + return; + } + for (InetAddress addr : answerList) { + Log.d(TAG, "Reported addr: " + addr.toString()); + } + mAnswers.clear(); + mAnswers.addAll(answerList); + mLatch.countDown(); + } + + @Override + public void onError(@NonNull DnsResolver.DnsException error) { + mErrorMsg = mMsg + error.getMessage(); + } + } + + public void testQueryForInetAddress() throws Exception { + doTestQueryForInetAddress(mExecutor); + } + + public void testQueryForInetAddressInline() throws Exception { + doTestQueryForInetAddress(mExecutorInline); + } + + public void testQueryForInetAddressIpv4() throws Exception { + doTestQueryForInetAddressIpv4(mExecutor); + } + + public void testQueryForInetAddressIpv4Inline() throws Exception { + doTestQueryForInetAddressIpv4(mExecutorInline); + } + + public void testQueryForInetAddressIpv6() throws Exception { + doTestQueryForInetAddressIpv6(mExecutor); + } + + public void testQueryForInetAddressIpv6Inline() throws Exception { + doTestQueryForInetAddressIpv6(mExecutorInline); + } + + public void testContinuousQueries() throws Exception { + doTestContinuousQueries(mExecutor); + } + + @SkipPresubmit(reason = "Flaky: b/159762682; add to presubmit after fixing") + public void testContinuousQueriesInline() throws Exception { + doTestContinuousQueries(mExecutorInline); + } + + public void doTestQueryForInetAddress(Executor executor) throws InterruptedException { + final String msg = "Test query for InetAddress " + TEST_DOMAIN; + for (Network network : getTestableNetworks()) { + final VerifyCancelInetAddressCallback callback = + new VerifyCancelInetAddressCallback(msg, null); + mDns.query(network, TEST_DOMAIN, FLAG_NO_CACHE_LOOKUP, executor, null, callback); + + assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.", + callback.waitForAnswer()); + callback.assertNoError(); + assertTrue(msg + " returned 0 results", !callback.isAnswerEmpty()); + } + } + + public void testQueryCancelForInetAddress() throws InterruptedException { + final String msg = "Test cancel query for InetAddress " + TEST_DOMAIN; + // Start a DNS query and the cancel it immediately. Use VerifyCancelInetAddressCallback to + // expect that the query is cancelled before it succeeds. If it is not cancelled before it + // succeeds, retry the test until it is. + for (Network network : getTestableNetworks()) { + boolean retry = false; + int round = 0; + do { + if (++round > CANCEL_RETRY_TIMES) { + fail(msg + " cancel failed " + CANCEL_RETRY_TIMES + " times"); + } + final CountDownLatch latch = new CountDownLatch(1); + final CancellationSignal cancelSignal = new CancellationSignal(); + final VerifyCancelInetAddressCallback callback = + new VerifyCancelInetAddressCallback(msg, cancelSignal); + mDns.query(network, TEST_DOMAIN, FLAG_EMPTY, mExecutor, cancelSignal, callback); + mExecutor.execute(() -> { + cancelSignal.cancel(); + latch.countDown(); + }); + + retry = callback.needRetry(); + assertTrue(msg + " query was not cancelled", + latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + } while (retry); + } + } + + public void doTestQueryForInetAddressIpv4(Executor executor) throws InterruptedException { + final String msg = "Test query for IPv4 InetAddress " + TEST_DOMAIN; + for (Network network : getTestableNetworks()) { + final VerifyCancelInetAddressCallback callback = + new VerifyCancelInetAddressCallback(msg, null); + mDns.query(network, TEST_DOMAIN, TYPE_A, FLAG_NO_CACHE_LOOKUP, + executor, null, callback); + + assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.", + callback.waitForAnswer()); + callback.assertNoError(); + assertTrue(msg + " returned 0 results", !callback.isAnswerEmpty()); + assertTrue(msg + " returned Ipv6 results", !callback.hasIpv6Answer()); + } + } + + public void doTestQueryForInetAddressIpv6(Executor executor) throws InterruptedException { + final String msg = "Test query for IPv6 InetAddress " + TEST_DOMAIN; + for (Network network : getTestableNetworks()) { + final VerifyCancelInetAddressCallback callback = + new VerifyCancelInetAddressCallback(msg, null); + mDns.query(network, TEST_DOMAIN, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP, + executor, null, callback); + + assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.", + callback.waitForAnswer()); + callback.assertNoError(); + assertTrue(msg + " returned 0 results", !callback.isAnswerEmpty()); + assertTrue(msg + " returned Ipv4 results", !callback.hasIpv4Answer()); + } + } + + public void testPrivateDnsBypass() throws InterruptedException { + final Network[] testNetworks = getTestableNetworks(); + + // Set an invalid private DNS server + mCtsNetUtils.setPrivateDnsStrictMode(INVALID_PRIVATE_DNS_SERVER); + final String msg = "Test PrivateDnsBypass " + TEST_DOMAIN; + for (Network network : testNetworks) { + // This test cannot be ran with null network because we need to explicitly pass a + // private DNS bypassable network or bind one. + if (network == null) continue; + + // wait for private DNS setting propagating + mCtsNetUtils.awaitPrivateDnsSetting(msg + " wait private DNS setting timeout", + network, INVALID_PRIVATE_DNS_SERVER, false); + + final CountDownLatch latch = new CountDownLatch(1); + final DnsResolver.Callback> errorCallback = + new DnsResolver.Callback>() { + @Override + public void onAnswer(@NonNull List answerList, int rcode) { + fail(msg + " should not get valid answer"); + } + + @Override + public void onError(@NonNull DnsResolver.DnsException error) { + assertEquals(DnsResolver.ERROR_SYSTEM, error.code); + assertEquals(ETIMEDOUT, ((ErrnoException) error.getCause()).errno); + latch.countDown(); + } + }; + // Private DNS strict mode with invalid DNS server is set + // Expect no valid answer returned but ErrnoException with ETIMEDOUT + mDns.query(network, TEST_DOMAIN, FLAG_NO_CACHE_LOOKUP, mExecutor, null, errorCallback); + + assertTrue(msg + " invalid server round. No response after " + TIMEOUT_MS + "ms.", + latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + + final VerifyCancelInetAddressCallback callback = + new VerifyCancelInetAddressCallback(msg, null); + // Bypass privateDns, expect query works fine + mDns.query(network.getPrivateDnsBypassingCopy(), + TEST_DOMAIN, FLAG_NO_CACHE_LOOKUP, mExecutor, null, callback); + + assertTrue(msg + " bypass private DNS round. No answer after " + TIMEOUT_MS + "ms.", + callback.waitForAnswer()); + callback.assertNoError(); + assertTrue(msg + " returned 0 results", !callback.isAnswerEmpty()); + + // To ensure private DNS bypass still work even if passing null network. + // Bind process network with a private DNS bypassable network. + mCM.bindProcessToNetwork(network.getPrivateDnsBypassingCopy()); + final VerifyCancelInetAddressCallback callbackWithNullNetwork = + new VerifyCancelInetAddressCallback(msg + " with null network ", null); + mDns.query(null, + TEST_DOMAIN, FLAG_NO_CACHE_LOOKUP, mExecutor, null, callbackWithNullNetwork); + + assertTrue(msg + " with null network bypass private DNS round. No answer after " + + TIMEOUT_MS + "ms.", callbackWithNullNetwork.waitForAnswer()); + callbackWithNullNetwork.assertNoError(); + assertTrue(msg + " with null network returned 0 results", + !callbackWithNullNetwork.isAnswerEmpty()); + + // Reset process network to default. + mCM.bindProcessToNetwork(null); + } + } + + public void doTestContinuousQueries(Executor executor) throws InterruptedException { + final String msg = "Test continuous " + QUERY_TIMES + " queries " + TEST_DOMAIN; + for (Network network : getTestableNetworks()) { + for (int i = 0; i < QUERY_TIMES ; ++i) { + final VerifyCancelInetAddressCallback callback = + new VerifyCancelInetAddressCallback(msg, null); + // query v6/v4 in turn + boolean queryV6 = (i % 2 == 0); + mDns.query(network, TEST_DOMAIN, queryV6 ? TYPE_AAAA : TYPE_A, + FLAG_NO_CACHE_LOOKUP, executor, null, callback); + + assertTrue(msg + " but no answer after " + TIMEOUT_MS + "ms.", + callback.waitForAnswer()); + callback.assertNoError(); + assertTrue(msg + " returned 0 results", !callback.isAnswerEmpty()); + assertTrue(msg + " returned " + (queryV6 ? "Ipv4" : "Ipv6") + " results", + queryV6 ? !callback.hasIpv4Answer() : !callback.hasIpv6Answer()); + } + } + } +} diff --git a/tests/cts/net/src/android/net/cts/DnsTest.java b/tests/cts/net/src/android/net/cts/DnsTest.java new file mode 100644 index 0000000000..fde27e9f12 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/DnsTest.java @@ -0,0 +1,309 @@ +/* + * 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.cts; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkInfo; +import android.os.SystemClock; +import android.test.AndroidTestCase; +import android.util.Log; + +import com.android.testutils.SkipPresubmit; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class DnsTest extends AndroidTestCase { + + static { + System.loadLibrary("nativedns_jni"); + } + + private static final boolean DBG = false; + private static final String TAG = "DnsTest"; + private static final String PROXY_NETWORK_TYPE = "PROXY"; + + private ConnectivityManager mCm; + + public void setUp() { + mCm = getContext().getSystemService(ConnectivityManager.class); + } + + /** + * @return true on success + */ + private static native boolean testNativeDns(); + + /** + * Verify: + * DNS works - forwards and backwards, giving ipv4 and ipv6 + * Test that DNS work on v4 and v6 networks + * Test Native dns calls (4) + * Todo: + * Cache is flushed when we change networks + * have per-network caches + * No cache when there's no network + * Perf - measure size of first and second tier caches and their effect + * Assert requires network permission + */ + @SkipPresubmit(reason = "IPv6 support may be missing on presubmit virtual hardware") + public void testDnsWorks() throws Exception { + ensureIpv6Connectivity(); + + InetAddress addrs[] = {}; + try { + addrs = InetAddress.getAllByName("www.google.com"); + } catch (UnknownHostException e) {} + assertTrue("[RERUN] DNS could not resolve www.google.com. Check internet connection", + addrs.length != 0); + boolean foundV4 = false, foundV6 = false; + for (InetAddress addr : addrs) { + if (addr instanceof Inet4Address) foundV4 = true; + else if (addr instanceof Inet6Address) foundV6 = true; + if (DBG) Log.e(TAG, "www.google.com gave " + addr.toString()); + } + + // We should have at least one of the addresses to connect! + assertTrue("www.google.com must have IPv4 and/or IPv6 address", foundV4 || foundV6); + + // Skip the rest of the test if the active network for watch is PROXY. + // TODO: Check NetworkInfo type in addition to type name once ag/601257 is merged. + if (getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH) + && activeNetworkInfoIsProxy()) { + Log.i(TAG, "Skipping test because the active network type name is PROXY."); + return; + } + + // Clear test state so we don't get confused with the previous results. + addrs = new InetAddress[0]; + foundV4 = foundV6 = false; + try { + addrs = InetAddress.getAllByName("ipv6.google.com"); + } catch (UnknownHostException e) {} + String msg = + "[RERUN] DNS could not resolve ipv6.google.com, check the network supports IPv6. lp=" + + mCm.getActiveLinkProperties(); + assertTrue(msg, addrs.length != 0); + for (InetAddress addr : addrs) { + msg = "[RERUN] ipv6.google.com returned IPv4 address: " + addr.getHostAddress() + + ", check your network's DNS server. lp=" + mCm.getActiveLinkProperties(); + assertFalse (msg, addr instanceof Inet4Address); + foundV6 |= (addr instanceof Inet6Address); + if (DBG) Log.e(TAG, "ipv6.google.com gave " + addr.toString()); + } + + assertTrue(foundV6); + + assertTrue(testNativeDns()); + } + + private static final String[] URLS = { "www.google.com", "ipv6.google.com", "www.yahoo.com", + "facebook.com", "youtube.com", "blogspot.com", "baidu.com", "wikipedia.org", +// live.com fails rev lookup. + "twitter.com", "qq.com", "msn.com", "yahoo.co.jp", "linkedin.com", + "taobao.com", "google.co.in", "sina.com.cn", "amazon.com", "wordpress.com", + "google.co.uk", "ebay.com", "yandex.ru", "163.com", "google.co.jp", "google.fr", + "microsoft.com", "paypal.com", "google.com.br", "flickr.com", + "mail.ru", "craigslist.org", "fc2.com", "google.it", +// "apple.com", fails rev lookup + "google.es", + "imdb.com", "google.ru", "soho.com", "bbc.co.uk", "vkontakte.ru", "ask.com", + "tumblr.com", "weibo.com", "go.com", "xvideos.com", "livejasmin.com", "cnn.com", + "youku.com", "blogspot.com", "soso.com", "google.ca", "aol.com", "tudou.com", + "xhamster.com", "megaupload.com", "ifeng.com", "zedo.com", "mediafire.com", "ameblo.jp", + "pornhub.com", "google.co.id", "godaddy.com", "adobe.com", "rakuten.co.jp", "about.com", + "espn.go.com", "4shared.com", "alibaba.com","ebay.de", "yieldmanager.com", + "wordpress.org", "livejournal.com", "google.com.tr", "google.com.mx", "renren.com", + "livedoor.com", "google.com.au", "youporn.com", "uol.com.br", "cnet.com", "conduit.com", + "google.pl", "myspace.com", "nytimes.com", "ebay.co.uk", "chinaz.com", "hao123.com", + "thepiratebay.org", "doubleclick.com", "alipay.com", "netflix.com", "cnzz.com", + "huffingtonpost.com", "twitpic.com", "weather.com", "babylon.com", "amazon.de", + "dailymotion.com", "orkut.com", "orkut.com.br", "google.com.sa", "odnoklassniki.ru", + "amazon.co.jp", "google.nl", "goo.ne.jp", "stumbleupon.com", "tube8.com", "tmall.com", + "imgur.com", "globo.com", "secureserver.net", "fileserve.com", "tianya.cn", "badoo.com", + "ehow.com", "photobucket.com", "imageshack.us", "xnxx.com", "deviantart.com", + "filestube.com", "addthis.com", "douban.com", "vimeo.com", "sogou.com", + "stackoverflow.com", "reddit.com", "dailymail.co.uk", "redtube.com", "megavideo.com", + "taringa.net", "pengyou.com", "amazon.co.uk", "fbcdn.net", "aweber.com", "spiegel.de", + "rapidshare.com", "mixi.jp", "360buy.com", "google.cn", "digg.com", "answers.com", + "bit.ly", "indiatimes.com", "skype.com", "yfrog.com", "optmd.com", "google.com.eg", + "google.com.pk", "58.com", "hotfile.com", "google.co.th", + "bankofamerica.com", "sourceforge.net", "maktoob.com", "warriorforum.com", "rediff.com", + "google.co.za", "56.com", "torrentz.eu", "clicksor.com", "avg.com", + "download.com", "ku6.com", "statcounter.com", "foxnews.com", "google.com.ar", + "nicovideo.jp", "reference.com", "liveinternet.ru", "ucoz.ru", "xinhuanet.com", + "xtendmedia.com", "naver.com", "youjizz.com", "domaintools.com", "sparkstudios.com", + "rambler.ru", "scribd.com", "kaixin001.com", "mashable.com", "adultfirendfinder.com", + "files.wordpress.com", "guardian.co.uk", "bild.de", "yelp.com", "wikimedia.org", + "chase.com", "onet.pl", "ameba.jp", "pconline.com.cn", "free.fr", "etsy.com", + "typepad.com", "youdao.com", "megaclick.com", "digitalpoint.com", "blogfa.com", + "salesforce.com", "adf.ly", "ganji.com", "wikia.com", "archive.org", "terra.com.br", + "w3schools.com", "ezinearticles.com", "wjs.com", "google.com.my", "clickbank.com", + "squidoo.com", "hulu.com", "repubblica.it", "google.be", "allegro.pl", "comcast.net", + "narod.ru", "zol.com.cn", "orange.fr", "soufun.com", "hatena.ne.jp", "google.gr", + "in.com", "techcrunch.com", "orkut.co.in", "xunlei.com", + "reuters.com", "google.com.vn", "hostgator.com", "kaskus.us", "espncricinfo.com", + "hootsuite.com", "qiyi.com", "gmx.net", "xing.com", "php.net", "soku.com", "web.de", + "libero.it", "groupon.com", "51.la", "slideshare.net", "booking.com", "seesaa.net", + "126.com", "telegraph.co.uk", "wretch.cc", "twimg.com", "rutracker.org", "angege.com", + "nba.com", "dell.com", "leboncoin.fr", "people.com", "google.com.tw", "walmart.com", + "daum.net", "2ch.net", "constantcontact.com", "nifty.com", "mywebsearch.com", + "tripadvisor.com", "google.se", "paipai.com", "google.com.ua", "ning.com", "hp.com", + "google.at", "joomla.org", "icio.us", "hudong.com", "csdn.net", "getfirebug.com", + "ups.com", "cj.com", "google.ch", "camzap.com", "wordreference.com", "tagged.com", + "wp.pl", "mozilla.com", "google.ru", "usps.com", "china.com", "themeforest.net", + "search-results.com", "tribalfusion.com", "thefreedictionary.com", "isohunt.com", + "linkwithin.com", "cam4.com", "plentyoffish.com", "wellsfargo.com", "metacafe.com", + "depositfiles.com", "freelancer.com", "opendns.com", "homeway.com", "engadget.com", + "10086.cn", "360.cn", "marca.com", "dropbox.com", "ign.com", "match.com", "google.pt", + "facemoods.com", "hardsextube.com", "google.com.ph", "lockerz.com", "istockphoto.com", + "partypoker.com", "netlog.com", "outbrain.com", "elpais.com", "fiverr.com", + "biglobe.ne.jp", "corriere.it", "love21cn.com", "yesky.com", "spankwire.com", + "ig.com.br", "imagevenue.com", "hubpages.com", "google.co.ve"}; + +// TODO - this works, but is slow and cts doesn't do anything with the result. +// Maybe require a min performance, a min cache size (detectable) and/or move +// to perf testing + private static final int LOOKUP_COUNT_GOAL = URLS.length; + public void skiptestDnsPerf() { + ArrayList results = new ArrayList(); + int failures = 0; + try { + for (int numberOfUrls = URLS.length; numberOfUrls > 0; numberOfUrls--) { + failures = 0; + int iterationLimit = LOOKUP_COUNT_GOAL / numberOfUrls; + long startTime = SystemClock.elapsedRealtimeNanos(); + for (int iteration = 0; iteration < iterationLimit; iteration++) { + for (int urlIndex = 0; urlIndex < numberOfUrls; urlIndex++) { + try { + InetAddress addr = InetAddress.getByName(URLS[urlIndex]); + } catch (UnknownHostException e) { + Log.e(TAG, "failed first lookup of " + URLS[urlIndex]); + failures++; + try { + InetAddress addr = InetAddress.getByName(URLS[urlIndex]); + } catch (UnknownHostException ee) { + failures++; + Log.e(TAG, "failed SECOND lookup of " + URLS[urlIndex]); + } + } + } + } + long endTime = SystemClock.elapsedRealtimeNanos(); + float nsPer = ((float)(endTime-startTime) / iterationLimit) / numberOfUrls/ 1000; + String thisResult = new String("getByName for " + numberOfUrls + " took " + + (endTime - startTime)/1000 + "(" + nsPer + ") with " + + failures + " failures\n"); + Log.d(TAG, thisResult); + results.add(thisResult); + } + // build up a list of addresses + ArrayList addressList = new ArrayList(); + for (String url : URLS) { + try { + InetAddress addr = InetAddress.getByName(url); + addressList.add(addr.getAddress()); + } catch (UnknownHostException e) { + Log.e(TAG, "Exception making reverseDNS list: " + e.toString()); + } + } + for (int numberOfAddrs = addressList.size(); numberOfAddrs > 0; numberOfAddrs--) { + int iterationLimit = LOOKUP_COUNT_GOAL / numberOfAddrs; + failures = 0; + long startTime = SystemClock.elapsedRealtimeNanos(); + for (int iteration = 0; iteration < iterationLimit; iteration++) { + for (int addrIndex = 0; addrIndex < numberOfAddrs; addrIndex++) { + try { + InetAddress addr = InetAddress.getByAddress(addressList.get(addrIndex)); + String hostname = addr.getHostName(); + } catch (UnknownHostException e) { + failures++; + Log.e(TAG, "Failure doing reverse DNS lookup: " + e.toString()); + try { + InetAddress addr = + InetAddress.getByAddress(addressList.get(addrIndex)); + String hostname = addr.getHostName(); + + } catch (UnknownHostException ee) { + failures++; + Log.e(TAG, "Failure doing SECOND reverse DNS lookup: " + + ee.toString()); + } + } + } + } + long endTime = SystemClock.elapsedRealtimeNanos(); + float nsPer = ((endTime-startTime) / iterationLimit) / numberOfAddrs / 1000; + String thisResult = new String("getHostName for " + numberOfAddrs + " took " + + (endTime - startTime)/1000 + "(" + nsPer + ") with " + + failures + " failures\n"); + Log.d(TAG, thisResult); + results.add(thisResult); + } + for (String result : results) Log.d(TAG, result); + + InetAddress exit = InetAddress.getByName("exitrightnow.com"); + Log.e(TAG, " exit address= "+exit.toString()); + + } catch (Exception e) { + Log.e(TAG, "bad URL in testDnsPerf: " + e.toString()); + } + } + + private boolean activeNetworkInfoIsProxy() { + NetworkInfo info = mCm.getActiveNetworkInfo(); + if (PROXY_NETWORK_TYPE.equals(info.getTypeName())) { + return true; + } + + return false; + } + + private void ensureIpv6Connectivity() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + final int TIMEOUT_MS = 5_000; + + final NetworkCallback callback = new NetworkCallback() { + @Override + public void onLinkPropertiesChanged(Network network, LinkProperties lp) { + if (lp.hasGlobalIpv6Address()) { + latch.countDown(); + } + } + }; + mCm.registerDefaultNetworkCallback(callback); + + String msg = "Default network did not provide IPv6 connectivity after " + TIMEOUT_MS + + "ms. Please connect to an IPv6-capable network. lp=" + + mCm.getActiveLinkProperties(); + try { + assertTrue(msg, latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + } finally { + mCm.unregisterNetworkCallback(callback); + } + } +} diff --git a/tests/cts/net/src/android/net/cts/IkeTunUtils.java b/tests/cts/net/src/android/net/cts/IkeTunUtils.java new file mode 100644 index 0000000000..fc25292b27 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/IkeTunUtils.java @@ -0,0 +1,188 @@ +/* + * 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.cts; + +import static android.net.cts.PacketUtils.BytePayload; +import static android.net.cts.PacketUtils.IP4_HDRLEN; +import static android.net.cts.PacketUtils.IP6_HDRLEN; +import static android.net.cts.PacketUtils.IpHeader; +import static android.net.cts.PacketUtils.UDP_HDRLEN; +import static android.net.cts.PacketUtils.UdpHeader; +import static android.net.cts.PacketUtils.getIpHeader; +import static android.system.OsConstants.IPPROTO_UDP; + +import android.os.ParcelFileDescriptor; + +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.util.Arrays; + +// TODO: Merge this with the version in the IPsec module (IKEv2 library) CTS tests. +/** An extension of the TunUtils class with IKE-specific packet handling. */ +public class IkeTunUtils extends TunUtils { + private static final int PORT_LEN = 2; + + private static final byte[] NON_ESP_MARKER = new byte[] {0, 0, 0, 0}; + + private static final int IKE_HEADER_LEN = 28; + private static final int IKE_SPI_LEN = 8; + private static final int IKE_IS_RESP_BYTE_OFFSET = 19; + private static final int IKE_MSG_ID_OFFSET = 20; + private static final int IKE_MSG_ID_LEN = 4; + + public IkeTunUtils(ParcelFileDescriptor tunFd) { + super(tunFd); + } + + /** + * Await an expected IKE request and inject an IKE response. + * + * @param respIkePkt IKE response packet without IP/UDP headers or NON ESP MARKER. + */ + public byte[] awaitReqAndInjectResp(long expectedInitIkeSpi, int expectedMsgId, + boolean encapExpected, byte[] respIkePkt) throws Exception { + final byte[] request = awaitIkePacket(expectedInitIkeSpi, expectedMsgId, encapExpected); + + // Build response header by flipping address and port + final InetAddress srcAddr = getDstAddress(request); + final InetAddress dstAddr = getSrcAddress(request); + final int srcPort = getDstPort(request); + final int dstPort = getSrcPort(request); + + final byte[] response = + buildIkePacket(srcAddr, dstAddr, srcPort, dstPort, encapExpected, respIkePkt); + injectPacket(response); + return request; + } + + private byte[] awaitIkePacket(long expectedInitIkeSpi, int expectedMsgId, boolean expectEncap) + throws Exception { + return super.awaitPacket(pkt -> isIke(pkt, expectedInitIkeSpi, expectedMsgId, expectEncap)); + } + + private static boolean isIke( + byte[] pkt, long expectedInitIkeSpi, int expectedMsgId, boolean encapExpected) { + final int ipProtocolOffset; + final int ikeOffset; + + if (isIpv6(pkt)) { + ipProtocolOffset = IP6_PROTO_OFFSET; + ikeOffset = IP6_HDRLEN + UDP_HDRLEN; + } else { + if (encapExpected && !hasNonEspMarkerv4(pkt)) { + return false; + } + + // Use default IPv4 header length (assuming no options) + final int encapMarkerLen = encapExpected ? NON_ESP_MARKER.length : 0; + ipProtocolOffset = IP4_PROTO_OFFSET; + ikeOffset = IP4_HDRLEN + UDP_HDRLEN + encapMarkerLen; + } + + return pkt[ipProtocolOffset] == IPPROTO_UDP + && areSpiAndMsgIdEqual(pkt, ikeOffset, expectedInitIkeSpi, expectedMsgId); + } + + /** Checks if the provided IPv4 packet has a UDP-encapsulation NON-ESP marker */ + private static boolean hasNonEspMarkerv4(byte[] ipv4Pkt) { + final int nonEspMarkerOffset = IP4_HDRLEN + UDP_HDRLEN; + if (ipv4Pkt.length < nonEspMarkerOffset + NON_ESP_MARKER.length) { + return false; + } + + final byte[] nonEspMarker = Arrays.copyOfRange( + ipv4Pkt, nonEspMarkerOffset, nonEspMarkerOffset + NON_ESP_MARKER.length); + return Arrays.equals(NON_ESP_MARKER, nonEspMarker); + } + + private static boolean areSpiAndMsgIdEqual( + byte[] pkt, int ikeOffset, long expectedIkeInitSpi, int expectedMsgId) { + if (pkt.length <= ikeOffset + IKE_HEADER_LEN) { + return false; + } + + final ByteBuffer buffer = ByteBuffer.wrap(pkt); + final long spi = buffer.getLong(ikeOffset); + final int msgId = buffer.getInt(ikeOffset + IKE_MSG_ID_OFFSET); + + return expectedIkeInitSpi == spi && expectedMsgId == msgId; + } + + private static InetAddress getSrcAddress(byte[] pkt) throws Exception { + return getAddress(pkt, true); + } + + private static InetAddress getDstAddress(byte[] pkt) throws Exception { + return getAddress(pkt, false); + } + + private static InetAddress getAddress(byte[] pkt, boolean getSrcAddr) throws Exception { + final int ipLen = isIpv6(pkt) ? IP6_ADDR_LEN : IP4_ADDR_LEN; + final int srcIpOffset = isIpv6(pkt) ? IP6_ADDR_OFFSET : IP4_ADDR_OFFSET; + final int ipOffset = getSrcAddr ? srcIpOffset : srcIpOffset + ipLen; + + if (pkt.length < ipOffset + ipLen) { + // Should be impossible; getAddress() is only called with a full IKE request including + // the IP and UDP headers. + throw new IllegalArgumentException("Packet was too short to contain IP address"); + } + + return InetAddress.getByAddress(Arrays.copyOfRange(pkt, ipOffset, ipOffset + ipLen)); + } + + private static int getSrcPort(byte[] pkt) throws Exception { + return getPort(pkt, true); + } + + private static int getDstPort(byte[] pkt) throws Exception { + return getPort(pkt, false); + } + + private static int getPort(byte[] pkt, boolean getSrcPort) { + final int srcPortOffset = isIpv6(pkt) ? IP6_HDRLEN : IP4_HDRLEN; + final int portOffset = getSrcPort ? srcPortOffset : srcPortOffset + PORT_LEN; + + if (pkt.length < portOffset + PORT_LEN) { + // Should be impossible; getPort() is only called with a full IKE request including the + // IP and UDP headers. + throw new IllegalArgumentException("Packet was too short to contain port"); + } + + final ByteBuffer buffer = ByteBuffer.wrap(pkt); + return Short.toUnsignedInt(buffer.getShort(portOffset)); + } + + private static byte[] buildIkePacket( + InetAddress srcAddr, + InetAddress dstAddr, + int srcPort, + int dstPort, + boolean useEncap, + byte[] payload) + throws Exception { + // Append non-ESP marker if encap is enabled + if (useEncap) { + final ByteBuffer buffer = ByteBuffer.allocate(NON_ESP_MARKER.length + payload.length); + buffer.put(NON_ESP_MARKER); + buffer.put(payload); + payload = buffer.array(); + } + + final UdpHeader udpPkt = new UdpHeader(srcPort, dstPort, new BytePayload(payload)); + final IpHeader ipPkt = getIpHeader(udpPkt.getProtocolId(), srcAddr, dstAddr, udpPkt); + return ipPkt.getPacketBytes(); + } +} diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java new file mode 100644 index 0000000000..9eab024cf0 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java @@ -0,0 +1,535 @@ +/* + * 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.cts; + +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.net.cts.util.CtsNetUtils.TestNetworkCallback; + +import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; + +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.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; + +import android.Manifest; +import android.annotation.NonNull; +import android.app.AppOpsManager; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.Ikev2VpnProfile; +import android.net.IpSecAlgorithm; +import android.net.LinkAddress; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.ProxyInfo; +import android.net.TestNetworkInterface; +import android.net.TestNetworkManager; +import android.net.VpnManager; +import android.net.cts.util.CtsNetUtils; +import android.os.Build; +import android.os.Process; +import android.platform.test.annotations.AppModeFull; + +import androidx.test.InstrumentationRegistry; + +import com.android.internal.util.HexDump; +import com.android.org.bouncycastle.x509.X509V1CertificateGenerator; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; +import com.android.testutils.DevSdkIgnoreRunner; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.math.BigInteger; +import java.net.InetAddress; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.security.auth.x500.X500Principal; + +@RunWith(DevSdkIgnoreRunner.class) +@IgnoreUpTo(Build.VERSION_CODES.Q) +@AppModeFull(reason = "Appops state changes disallowed for instant apps (OP_ACTIVATE_PLATFORM_VPN)") +public class Ikev2VpnTest { + private static final String TAG = Ikev2VpnTest.class.getSimpleName(); + + // Test vectors for IKE negotiation in test mode. + private static final String SUCCESSFUL_IKE_INIT_RESP_V4 = + "46b8eca1e0d72a18b2b5d9006d47a0022120222000000000000002d0220000300000002c01010004030000" + + "0c0100000c800e0100030000080300000c030000080200000400000008040000102800020800" + + "100000b8070f159fe5141d8754ca86f72ecc28d66f514927e96cbe9eec0adb42bf2c276a0ab7" + + "a97fa93555f4be9218c14e7f286bb28c6b4fb13825a420f2ffc165854f200bab37d69c8963d4" + + "0acb831d983163aa50622fd35c182efe882cf54d6106222abcfaa597255d302f1b95ab71c142" + + "c279ea5839a180070bff73f9d03fab815f0d5ee2adec7e409d1e35979f8bd92ffd8aab13d1a0" + + "0657d816643ae767e9ae84d2ccfa2bcce1a50572be8d3748ae4863c41ae90da16271e014270f" + + "77edd5cd2e3299f3ab27d7203f93d770bacf816041cdcecd0f9af249033979da4369cb242dd9" + + "6d172e60513ff3db02de63e50eb7d7f596ada55d7946cad0af0669d1f3e2804846ab3f2a930d" + + "df56f7f025f25c25ada694e6231abbb87ee8cfd072c8481dc0b0f6b083fdc3bd89b080e49feb" + + "0288eef6fdf8a26ee2fc564a11e7385215cf2deaf2a9965638fc279c908ccdf04094988d91a2" + + "464b4a8c0326533aff5119ed79ecbd9d99a218b44f506a5eb09351e67da86698b4c58718db25" + + "d55f426fb4c76471b27a41fbce00777bc233c7f6e842e39146f466826de94f564cad8b92bfbe" + + "87c99c4c7973ec5f1eea8795e7da82819753aa7c4fcfdab77066c56b939330c4b0d354c23f83" + + "ea82fa7a64c4b108f1188379ea0eb4918ee009d804100e6bf118771b9058d42141c847d5ec37" + + "6e5ec591c71fc9dac01063c2bd31f9c783b28bf1182900002430f3d5de3449462b31dd28bc27" + + "297b6ad169bccce4f66c5399c6e0be9120166f2900001c0000400428b8df2e66f69c8584a186" + + "c5eac66783551d49b72900001c000040054e7a622e802d5cbfb96d5f30a6e433994370173529" + + "0000080000402e290000100000402f00020003000400050000000800004014"; + private static final String SUCCESSFUL_IKE_INIT_RESP_V6 = + "46b8eca1e0d72a1800d9ea1babce26bf2120222000000000000002d0220000300000002c01010004030000" + + "0c0100000c800e0100030000080300000c030000080200000400000008040000102800020800" + + "100000ea0e6dd9ca5930a9a45c323a41f64bfd8cdef7730f5fbff37d7c377da427f489a42aa8" + + "c89233380e6e925990d49de35c2cdcf63a61302c731a4b3569df1ee1bf2457e55a6751838ede" + + "abb75cc63ba5c9e4355e8e784f383a5efe8a44727dc14aeaf8dacc2620fb1c8875416dc07739" + + "7fe4decc1bd514a9c7d270cf21fd734c63a25c34b30b68686e54e8a198f37f27cb491fe27235" + + "fab5476b036d875ccab9a68d65fbf3006197f9bebbf94de0d3802b4fafe1d48d931ce3a1a346" + + "2d65bd639e9bd7fa46299650a9dbaf9b324e40b466942d91a59f41ef8042f8474c4850ed0f63" + + "e9238949d41cd8bbaea9aefdb65443a6405792839563aa5dc5c36b5ce8326ccf8a94d9622b85" + + "038d390d5fc0299e14e1f022966d4ac66515f6108ca04faec44821fe5bbf2ed4f84ff5671219" + + "608cb4c36b44a31ba010c9088f8d5ff943bb9ff857f74be1755f57a5783874adc57f42bb174e" + + "4ad3215de628707014dbcb1707bd214658118fdd7a42b3e1638b991ce5b812a667f1145be811" + + "685e3cd3baf9b18d062657b64c206a4d19a531c252a6a51a04aeaf42c618620cdbab65baca23" + + "82c57ed888422aeaacf7f1bc3fe2247ff7e7eaca218b74d7b31d02f2b0afa123f802529e7e6c" + + "3259d418290740ddbf55686e26998d7edcbbf895664972fed666f2f20af40503aa2af436ec6d" + + "4ec981ab19b9088755d94ae7a7c2066ea331d4e56e290000243fefe5555fce552d57a84e682c" + + "d4a6dfb3f2f94a94464d5bec3d88b88e9559642900001c00004004eb4afff764e7b79bca78b1" + + "3a89100d36d678ae982900001c00004005d177216a3c26f782076e12570d40bfaaa148822929" + + "0000080000402e290000100000402f00020003000400050000000800004014"; + private static final String SUCCESSFUL_IKE_AUTH_RESP_V4 = + "46b8eca1e0d72a18b2b5d9006d47a0022e20232000000001000000e0240000c420a2500a3da4c66fa6929e" + + "600f36349ba0e38de14f78a3ad0416cba8c058735712a3d3f9a0a6ed36de09b5e9e02697e7c4" + + "2d210ac86cfbd709503cfa51e2eab8cfdc6427d136313c072968f6506a546eb5927164200592" + + "6e36a16ee994e63f029432a67bc7d37ca619e1bd6e1678df14853067ecf816b48b81e8746069" + + "406363e5aa55f13cb2afda9dbebee94256c29d630b17dd7f1ee52351f92b6e1c3d8551c513f1" + + "d74ac52a80b2041397e109fe0aeb3c105b0d4be0ae343a943398764281"; + private static final String SUCCESSFUL_IKE_AUTH_RESP_V6 = + "46b8eca1e0d72a1800d9ea1babce26bf2e20232000000001000000f0240000d4aaf6eaa6c06b50447e6f54" + + "827fd8a9d9d6ac8015c1ebb3e8cb03fc6e54b49a107441f50004027cc5021600828026367f03" + + "bc425821cd7772ee98637361300c9b76056e874fea2bd4a17212370b291894264d8c023a01d1" + + "c3b691fd4b7c0b534e8c95af4c4638e2d125cb21c6267e2507cd745d72e8da109c47b9259c6c" + + "57a26f6bc5b337b9b9496d54bdde0333d7a32e6e1335c9ee730c3ecd607a8689aa7b0577b74f" + + "3bf437696a9fd5fc0aee3ed346cd9e15d1dda293df89eb388a8719388a60ca7625754de12cdb" + + "efe4c886c5c401"; + private static final long IKE_INITIATOR_SPI = Long.parseLong("46B8ECA1E0D72A18", 16); + + private static final InetAddress LOCAL_OUTER_4 = InetAddress.parseNumericAddress("192.0.2.1"); + private static final InetAddress LOCAL_OUTER_6 = + InetAddress.parseNumericAddress("2001:db8::1"); + + private static final int IP4_PREFIX_LEN = 32; + private static final int IP6_PREFIX_LEN = 128; + + // TODO: Use IPv6 address when we can generate test vectors (GCE does not allow IPv6 yet). + private static final String TEST_SERVER_ADDR_V4 = "192.0.2.2"; + private static final String TEST_SERVER_ADDR_V6 = "2001:db8::2"; + private static final String TEST_IDENTITY = "client.cts.android.com"; + private static final List TEST_ALLOWED_ALGORITHMS = + Arrays.asList(IpSecAlgorithm.AUTH_CRYPT_AES_GCM); + + private static final ProxyInfo TEST_PROXY_INFO = + ProxyInfo.buildDirectProxy("proxy.cts.android.com", 1234); + private static final int TEST_MTU = 1300; + + private static final byte[] TEST_PSK = "ikeAndroidPsk".getBytes(); + private static final String TEST_USER = "username"; + private static final String TEST_PASSWORD = "pa55w0rd"; + + // Static state to reduce setup/teardown + private static final Context sContext = InstrumentationRegistry.getContext(); + private static final ConnectivityManager sCM = + (ConnectivityManager) sContext.getSystemService(Context.CONNECTIVITY_SERVICE); + private static final VpnManager sVpnMgr = + (VpnManager) sContext.getSystemService(Context.VPN_MANAGEMENT_SERVICE); + private static final CtsNetUtils mCtsNetUtils = new CtsNetUtils(sContext); + + private final X509Certificate mServerRootCa; + private final CertificateAndKey mUserCertKey; + + public Ikev2VpnTest() throws Exception { + // Build certificates + mServerRootCa = generateRandomCertAndKeyPair().cert; + mUserCertKey = generateRandomCertAndKeyPair(); + } + + @After + public void tearDown() { + setAppop(AppOpsManager.OP_ACTIVATE_VPN, false); + setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, false); + } + + /** + * Sets the given appop using shell commands + * + *

This method must NEVER be called from within a shell permission, as it will attempt to + * acquire, and then drop the shell permission identity. This results in the caller losing the + * shell permission identity due to these calls not being reference counted. + */ + public void setAppop(int appop, boolean allow) { + // Requires shell permission to update appops. + runWithShellPermissionIdentity(() -> { + mCtsNetUtils.setAppopPrivileged(appop, allow); + }, Manifest.permission.MANAGE_TEST_NETWORKS); + } + + private Ikev2VpnProfile buildIkev2VpnProfileCommon( + Ikev2VpnProfile.Builder builder, boolean isRestrictedToTestNetworks) throws Exception { + if (isRestrictedToTestNetworks) { + builder.restrictToTestNetworks(); + } + + return builder.setBypassable(true) + .setAllowedAlgorithms(TEST_ALLOWED_ALGORITHMS) + .setProxy(TEST_PROXY_INFO) + .setMaxMtu(TEST_MTU) + .setMetered(false) + .build(); + } + + private Ikev2VpnProfile buildIkev2VpnProfilePsk(boolean isRestrictedToTestNetworks) + throws Exception { + return buildIkev2VpnProfilePsk(TEST_SERVER_ADDR_V6, isRestrictedToTestNetworks); + } + + private Ikev2VpnProfile buildIkev2VpnProfilePsk( + String remote, boolean isRestrictedToTestNetworks) throws Exception { + final Ikev2VpnProfile.Builder builder = + new Ikev2VpnProfile.Builder(remote, TEST_IDENTITY).setAuthPsk(TEST_PSK); + + return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks); + } + + private Ikev2VpnProfile buildIkev2VpnProfileUsernamePassword(boolean isRestrictedToTestNetworks) + throws Exception { + final Ikev2VpnProfile.Builder builder = + new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY) + .setAuthUsernamePassword(TEST_USER, TEST_PASSWORD, mServerRootCa); + + return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks); + } + + private Ikev2VpnProfile buildIkev2VpnProfileDigitalSignature(boolean isRestrictedToTestNetworks) + throws Exception { + final Ikev2VpnProfile.Builder builder = + new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY) + .setAuthDigitalSignature( + mUserCertKey.cert, mUserCertKey.key, mServerRootCa); + + return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks); + } + + private void checkBasicIkev2VpnProfile(@NonNull Ikev2VpnProfile profile) throws Exception { + assertEquals(TEST_SERVER_ADDR_V6, profile.getServerAddr()); + assertEquals(TEST_IDENTITY, profile.getUserIdentity()); + assertEquals(TEST_PROXY_INFO, profile.getProxyInfo()); + assertEquals(TEST_ALLOWED_ALGORITHMS, profile.getAllowedAlgorithms()); + assertTrue(profile.isBypassable()); + assertFalse(profile.isMetered()); + assertEquals(TEST_MTU, profile.getMaxMtu()); + assertFalse(profile.isRestrictedToTestNetworks()); + } + + @Test + public void testBuildIkev2VpnProfilePsk() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + final Ikev2VpnProfile profile = + buildIkev2VpnProfilePsk(false /* isRestrictedToTestNetworks */); + + checkBasicIkev2VpnProfile(profile); + assertArrayEquals(TEST_PSK, profile.getPresharedKey()); + + // Verify nothing else is set. + assertNull(profile.getUsername()); + assertNull(profile.getPassword()); + assertNull(profile.getServerRootCaCert()); + assertNull(profile.getRsaPrivateKey()); + assertNull(profile.getUserCert()); + } + + @Test + public void testBuildIkev2VpnProfileUsernamePassword() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + final Ikev2VpnProfile profile = + buildIkev2VpnProfileUsernamePassword(false /* isRestrictedToTestNetworks */); + + checkBasicIkev2VpnProfile(profile); + assertEquals(TEST_USER, profile.getUsername()); + assertEquals(TEST_PASSWORD, profile.getPassword()); + assertEquals(mServerRootCa, profile.getServerRootCaCert()); + + // Verify nothing else is set. + assertNull(profile.getPresharedKey()); + assertNull(profile.getRsaPrivateKey()); + assertNull(profile.getUserCert()); + } + + @Test + public void testBuildIkev2VpnProfileDigitalSignature() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + final Ikev2VpnProfile profile = + buildIkev2VpnProfileDigitalSignature(false /* isRestrictedToTestNetworks */); + + checkBasicIkev2VpnProfile(profile); + assertEquals(mUserCertKey.cert, profile.getUserCert()); + assertEquals(mUserCertKey.key, profile.getRsaPrivateKey()); + assertEquals(mServerRootCa, profile.getServerRootCaCert()); + + // Verify nothing else is set. + assertNull(profile.getUsername()); + assertNull(profile.getPassword()); + assertNull(profile.getPresharedKey()); + } + + private void verifyProvisionVpnProfile( + boolean hasActivateVpn, boolean hasActivatePlatformVpn, boolean expectIntent) + throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + setAppop(AppOpsManager.OP_ACTIVATE_VPN, hasActivateVpn); + setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, hasActivatePlatformVpn); + + final Ikev2VpnProfile profile = + buildIkev2VpnProfilePsk(false /* isRestrictedToTestNetworks */); + final Intent intent = sVpnMgr.provisionVpnProfile(profile); + assertEquals(expectIntent, intent != null); + } + + @Test + public void testProvisionVpnProfileNoPreviousConsent() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + verifyProvisionVpnProfile(false /* hasActivateVpn */, + false /* hasActivatePlatformVpn */, true /* expectIntent */); + } + + @Test + public void testProvisionVpnProfilePlatformVpnConsented() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + verifyProvisionVpnProfile(false /* hasActivateVpn */, + true /* hasActivatePlatformVpn */, false /* expectIntent */); + } + + @Test + public void testProvisionVpnProfileVpnServiceConsented() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + verifyProvisionVpnProfile(true /* hasActivateVpn */, + false /* hasActivatePlatformVpn */, false /* expectIntent */); + } + + @Test + public void testProvisionVpnProfileAllPreConsented() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + verifyProvisionVpnProfile(true /* hasActivateVpn */, + true /* hasActivatePlatformVpn */, false /* expectIntent */); + } + + @Test + public void testDeleteVpnProfile() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, true); + + final Ikev2VpnProfile profile = + buildIkev2VpnProfilePsk(false /* isRestrictedToTestNetworks */); + assertNull(sVpnMgr.provisionVpnProfile(profile)); + + // Verify that deleting the profile works (even without the appop) + setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, false); + sVpnMgr.deleteProvisionedVpnProfile(); + + // Test that the profile was deleted - starting it should throw an IAE. + try { + setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, true); + sVpnMgr.startProvisionedVpnProfile(); + fail("Expected IllegalArgumentException due to missing profile"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testStartVpnProfileNoPreviousConsent() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + setAppop(AppOpsManager.OP_ACTIVATE_VPN, false); + setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, false); + + // Make sure the VpnProfile is not provisioned already. + sVpnMgr.stopProvisionedVpnProfile(); + + try { + sVpnMgr.startProvisionedVpnProfile(); + fail("Expected SecurityException for missing consent"); + } catch (SecurityException expected) { + } + } + + private void checkStartStopVpnProfileBuildsNetworks(IkeTunUtils tunUtils, boolean testIpv6) + throws Exception { + String serverAddr = testIpv6 ? TEST_SERVER_ADDR_V6 : TEST_SERVER_ADDR_V4; + String initResp = testIpv6 ? SUCCESSFUL_IKE_INIT_RESP_V6 : SUCCESSFUL_IKE_INIT_RESP_V4; + String authResp = testIpv6 ? SUCCESSFUL_IKE_AUTH_RESP_V6 : SUCCESSFUL_IKE_AUTH_RESP_V4; + boolean hasNat = !testIpv6; + + // Requires MANAGE_TEST_NETWORKS to provision a test-mode profile. + mCtsNetUtils.setAppopPrivileged(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, true); + + final Ikev2VpnProfile profile = + buildIkev2VpnProfilePsk(serverAddr, true /* isRestrictedToTestNetworks */); + assertNull(sVpnMgr.provisionVpnProfile(profile)); + + sVpnMgr.startProvisionedVpnProfile(); + + // Inject IKE negotiation + int expectedMsgId = 0; + tunUtils.awaitReqAndInjectResp(IKE_INITIATOR_SPI, expectedMsgId++, false /* isEncap */, + HexDump.hexStringToByteArray(initResp)); + tunUtils.awaitReqAndInjectResp(IKE_INITIATOR_SPI, expectedMsgId++, hasNat /* isEncap */, + HexDump.hexStringToByteArray(authResp)); + + // Verify the VPN network came up + final NetworkRequest nr = new NetworkRequest.Builder() + .clearCapabilities().addTransportType(TRANSPORT_VPN).build(); + + final TestNetworkCallback cb = new TestNetworkCallback(); + sCM.requestNetwork(nr, cb); + cb.waitForAvailable(); + final Network vpnNetwork = cb.currentNetwork; + assertNotNull(vpnNetwork); + + final NetworkCapabilities caps = sCM.getNetworkCapabilities(vpnNetwork); + assertTrue(caps.hasTransport(TRANSPORT_VPN)); + assertTrue(caps.hasCapability(NET_CAPABILITY_INTERNET)); + assertEquals(Process.myUid(), caps.getOwnerUid()); + + sVpnMgr.stopProvisionedVpnProfile(); + cb.waitForLost(); + assertEquals(vpnNetwork, cb.lastLostNetwork); + } + + private void doTestStartStopVpnProfile(boolean testIpv6) throws Exception { + // Non-final; these variables ensure we clean up properly after our test if we have + // allocated test network resources + final TestNetworkManager tnm = sContext.getSystemService(TestNetworkManager.class); + TestNetworkInterface testIface = null; + TestNetworkCallback tunNetworkCallback = null; + + try { + // Build underlying test network + testIface = tnm.createTunInterface( + new LinkAddress[] { + new LinkAddress(LOCAL_OUTER_4, IP4_PREFIX_LEN), + new LinkAddress(LOCAL_OUTER_6, IP6_PREFIX_LEN)}); + + // Hold on to this callback to ensure network does not get reaped. + tunNetworkCallback = mCtsNetUtils.setupAndGetTestNetwork( + testIface.getInterfaceName()); + final IkeTunUtils tunUtils = new IkeTunUtils(testIface.getFileDescriptor()); + + checkStartStopVpnProfileBuildsNetworks(tunUtils, testIpv6); + } finally { + // Make sure to stop the VPN profile. This is safe to call multiple times. + sVpnMgr.stopProvisionedVpnProfile(); + + if (testIface != null) { + testIface.getFileDescriptor().close(); + } + + if (tunNetworkCallback != null) { + sCM.unregisterNetworkCallback(tunNetworkCallback); + } + + final Network testNetwork = tunNetworkCallback.currentNetwork; + if (testNetwork != null) { + tnm.teardownTestNetwork(testNetwork); + } + } + } + + @Test + public void testStartStopVpnProfileV4() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + // Requires shell permission to update appops. + runWithShellPermissionIdentity(() -> { + doTestStartStopVpnProfile(false); + }); + } + + @Test + public void testStartStopVpnProfileV6() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + // Requires shell permission to update appops. + runWithShellPermissionIdentity(() -> { + doTestStartStopVpnProfile(true); + }); + } + + 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/cts/net/src/android/net/cts/InetAddressesTest.java b/tests/cts/net/src/android/net/cts/InetAddressesTest.java new file mode 100644 index 0000000000..7837ce9ed5 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/InetAddressesTest.java @@ -0,0 +1,134 @@ +/* + * 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.cts; + +import android.net.InetAddresses; +import java.net.InetAddress; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@RunWith(JUnitParamsRunner.class) +public class InetAddressesTest { + + public static String[][] validNumericAddressesAndStringRepresentation() { + return new String[][] { + // Regular IPv4. + { "1.2.3.4", "1.2.3.4" }, + + // Regular IPv6. + { "2001:4860:800d::68", "2001:4860:800d::68" }, + { "1234:5678::9ABC:DEF0", "1234:5678::9abc:def0" }, + { "2001:cdba:9abc:5678::", "2001:cdba:9abc:5678::" }, + { "::2001:cdba:9abc:5678", "::2001:cdba:9abc:5678" }, + { "64:ff9b::1.2.3.4", "64:ff9b::102:304" }, + + { "::9abc:5678", "::154.188.86.120" }, + + // Mapped IPv4 + { "::ffff:127.0.0.1", "127.0.0.1" }, + + // Android does not recognize Octal (leading 0) cases: they are treated as decimal. + { "0177.00.00.01", "177.0.0.1" }, + + // Verify that examples from JavaDoc work correctly. + { "192.0.2.1", "192.0.2.1" }, + { "2001:db8::1:2", "2001:db8::1:2" }, + }; + } + + public static String[] invalidNumericAddresses() { + return new String[] { + "", + " ", + "\t", + "\n", + "1.2.3.4.", + "1.2.3", + "1.2", + "1", + "1234", + "0", + "0x1.0x2.0x3.0x4", + "0x7f.0x00.0x00.0x01", + "0256.00.00.01", + "fred", + "www.google.com", + // IPv6 encoded for use in URL as defined in RFC 2732 + "[fe80::6:2222]", + }; + } + + @Parameters(method = "validNumericAddressesAndStringRepresentation") + @Test + public void parseNumericAddress(String address, String expectedString) { + InetAddress inetAddress = InetAddresses.parseNumericAddress(address); + assertEquals(expectedString, inetAddress.getHostAddress()); + } + + @Parameters(method = "invalidNumericAddresses") + @Test + public void test_parseNonNumericAddress(String address) { + try { + InetAddress inetAddress = InetAddresses.parseNumericAddress(address); + fail(String.format( + "Address %s is not numeric but was parsed as %s", address, inetAddress)); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).contains(address); + } + } + + @Test + public void test_parseNumericAddress_null() { + try { + InetAddress inetAddress = InetAddresses.parseNumericAddress(null); + fail(String.format("null is not numeric but was parsed as %s", inetAddress)); + } catch (NullPointerException e) { + // expected + } + } + + @Parameters(method = "validNumericAddressesAndStringRepresentation") + @Test + public void test_isNumericAddress(String address, String unused) { + assertTrue("expected '" + address + "' to be treated as numeric", + InetAddresses.isNumericAddress(address)); + } + + @Parameters(method = "invalidNumericAddresses") + @Test + public void test_isNotNumericAddress(String address) { + assertFalse("expected '" + address + "' to be treated as non-numeric", + InetAddresses.isNumericAddress(address)); + } + + @Test + public void test_isNumericAddress_null() { + try { + InetAddresses.isNumericAddress(null); + fail("expected null to throw a NullPointerException"); + } catch (NullPointerException e) { + // expected + } + } +} diff --git a/tests/cts/net/src/android/net/cts/IpConfigurationTest.java b/tests/cts/net/src/android/net/cts/IpConfigurationTest.java new file mode 100644 index 0000000000..56ab2a7531 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/IpConfigurationTest.java @@ -0,0 +1,123 @@ +/* + * 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.cts; + +import static com.android.testutils.ParcelUtils.assertParcelSane; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import android.net.IpConfiguration; +import android.net.LinkAddress; +import android.net.ProxyInfo; +import android.net.StaticIpConfiguration; + +import androidx.test.runner.AndroidJUnit4; + +import libcore.net.InetAddressUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.InetAddress; +import java.util.ArrayList; + +@RunWith(AndroidJUnit4.class) +public final class IpConfigurationTest { + private static final LinkAddress LINKADDR = new LinkAddress("192.0.2.2/25"); + private static final InetAddress GATEWAY = InetAddressUtils.parseNumericAddress("192.0.2.1"); + private static final InetAddress DNS1 = InetAddressUtils.parseNumericAddress("8.8.8.8"); + private static final InetAddress DNS2 = InetAddressUtils.parseNumericAddress("8.8.4.4"); + private static final String DOMAINS = "example.com"; + + private static final ArrayList dnsServers = new ArrayList<>(); + + private StaticIpConfiguration mStaticIpConfig; + private ProxyInfo mProxy; + + @Before + public void setUp() { + dnsServers.add(DNS1); + dnsServers.add(DNS2); + mStaticIpConfig = new StaticIpConfiguration.Builder() + .setIpAddress(LINKADDR) + .setGateway(GATEWAY) + .setDnsServers(dnsServers) + .setDomains(DOMAINS) + .build(); + + mProxy = ProxyInfo.buildDirectProxy("test", 8888); + } + + @Test + public void testConstructor() { + IpConfiguration ipConfig = new IpConfiguration(); + checkEmpty(ipConfig); + assertIpConfigurationEqual(ipConfig, new IpConfiguration()); + assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig)); + + ipConfig.setStaticIpConfiguration(mStaticIpConfig); + ipConfig.setHttpProxy(mProxy); + + ipConfig.setIpAssignment(IpConfiguration.IpAssignment.STATIC); + ipConfig.setProxySettings(IpConfiguration.ProxySettings.PAC); + assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig)); + + ipConfig.setIpAssignment(IpConfiguration.IpAssignment.STATIC); + ipConfig.setProxySettings(IpConfiguration.ProxySettings.STATIC); + assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig)); + + ipConfig.setIpAssignment(IpConfiguration.IpAssignment.DHCP); + ipConfig.setProxySettings(IpConfiguration.ProxySettings.PAC); + assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig)); + + ipConfig.setIpAssignment(IpConfiguration.IpAssignment.DHCP); + ipConfig.setProxySettings(IpConfiguration.ProxySettings.PAC); + assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig)); + + ipConfig.setIpAssignment(IpConfiguration.IpAssignment.DHCP); + ipConfig.setProxySettings(IpConfiguration.ProxySettings.STATIC); + assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig)); + + ipConfig.setIpAssignment(IpConfiguration.IpAssignment.DHCP); + ipConfig.setProxySettings(IpConfiguration.ProxySettings.NONE); + assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig)); + } + + private void checkEmpty(IpConfiguration config) { + assertEquals(IpConfiguration.IpAssignment.UNASSIGNED, + config.getIpAssignment().UNASSIGNED); + assertEquals(IpConfiguration.ProxySettings.UNASSIGNED, + config.getProxySettings().UNASSIGNED); + assertNull(config.getStaticIpConfiguration()); + assertNull(config.getHttpProxy()); + } + + private void assertIpConfigurationEqual(IpConfiguration source, IpConfiguration target) { + assertEquals(source.getIpAssignment(), target.getIpAssignment()); + assertEquals(source.getProxySettings(), target.getProxySettings()); + assertEquals(source.getHttpProxy(), target.getHttpProxy()); + assertEquals(source.getStaticIpConfiguration(), target.getStaticIpConfiguration()); + } + + @Test + public void testParcel() { + final IpConfiguration config = new IpConfiguration(); + assertParcelSane(config, 4); + } +} diff --git a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java new file mode 100644 index 0000000000..10e43e7b6a --- /dev/null +++ b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java @@ -0,0 +1,556 @@ +/* + * 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.cts; + +import static org.junit.Assert.assertArrayEquals; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.IpSecAlgorithm; +import android.net.IpSecManager; +import android.net.IpSecTransform; +import android.platform.test.annotations.AppModeFull; +import android.system.Os; +import android.system.OsConstants; +import android.util.Log; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class IpSecBaseTest { + + private static final String TAG = IpSecBaseTest.class.getSimpleName(); + + protected static final String IPV4_LOOPBACK = "127.0.0.1"; + protected static final String IPV6_LOOPBACK = "::1"; + protected static final String[] LOOPBACK_ADDRS = new String[] {IPV4_LOOPBACK, IPV6_LOOPBACK}; + protected static final int[] DIRECTIONS = + new int[] {IpSecManager.DIRECTION_IN, IpSecManager.DIRECTION_OUT}; + + protected static final byte[] TEST_DATA = "Best test data ever!".getBytes(); + protected static final int DATA_BUFFER_LEN = 4096; + protected static final int SOCK_TIMEOUT = 500; + + private static final byte[] KEY_DATA = { + 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, + 0x20, 0x21, 0x22, 0x23 + }; + + protected static final byte[] AUTH_KEY = getKey(256); + protected static final byte[] CRYPT_KEY = getKey(256); + + protected ConnectivityManager mCM; + protected IpSecManager mISM; + + @Before + public void setUp() throws Exception { + mISM = + (IpSecManager) + InstrumentationRegistry.getContext() + .getSystemService(Context.IPSEC_SERVICE); + mCM = + (ConnectivityManager) + InstrumentationRegistry.getContext() + .getSystemService(Context.CONNECTIVITY_SERVICE); + } + + protected static byte[] getKey(int bitLength) { + return Arrays.copyOf(KEY_DATA, bitLength / 8); + } + + protected static int getDomain(InetAddress address) { + int domain; + if (address instanceof Inet6Address) { + domain = OsConstants.AF_INET6; + } else { + domain = OsConstants.AF_INET; + } + return domain; + } + + protected static int getPort(FileDescriptor sock) throws Exception { + return ((InetSocketAddress) Os.getsockname(sock)).getPort(); + } + + public static interface GenericSocket extends AutoCloseable { + void send(byte[] data) throws Exception; + + byte[] receive() throws Exception; + + int getPort() throws Exception; + + void close() throws Exception; + + void applyTransportModeTransform( + IpSecManager ism, int direction, IpSecTransform transform) throws Exception; + + void removeTransportModeTransforms(IpSecManager ism) throws Exception; + } + + public static interface GenericTcpSocket extends GenericSocket {} + + public static interface GenericUdpSocket extends GenericSocket { + void sendTo(byte[] data, InetAddress dstAddr, int port) throws Exception; + } + + public abstract static class NativeSocket implements GenericSocket { + public FileDescriptor mFd; + + public NativeSocket(FileDescriptor fd) { + mFd = fd; + } + + @Override + public void send(byte[] data) throws Exception { + Os.write(mFd, data, 0, data.length); + } + + @Override + public byte[] receive() throws Exception { + byte[] in = new byte[DATA_BUFFER_LEN]; + AtomicInteger bytesRead = new AtomicInteger(-1); + + Thread readSockThread = new Thread(() -> { + long startTime = System.currentTimeMillis(); + while (bytesRead.get() < 0 && System.currentTimeMillis() < startTime + SOCK_TIMEOUT) { + try { + bytesRead.set(Os.recvfrom(mFd, in, 0, DATA_BUFFER_LEN, 0, null)); + } catch (Exception e) { + Log.e(TAG, "Error encountered reading from socket", e); + } + } + }); + + readSockThread.start(); + readSockThread.join(SOCK_TIMEOUT); + + if (bytesRead.get() < 0) { + throw new IOException("No data received from socket"); + } + + return Arrays.copyOfRange(in, 0, bytesRead.get()); + } + + @Override + public int getPort() throws Exception { + return IpSecBaseTest.getPort(mFd); + } + + @Override + public void close() throws Exception { + Os.close(mFd); + } + + @Override + public void applyTransportModeTransform( + IpSecManager ism, int direction, IpSecTransform transform) throws Exception { + ism.applyTransportModeTransform(mFd, direction, transform); + } + + @Override + public void removeTransportModeTransforms(IpSecManager ism) throws Exception { + ism.removeTransportModeTransforms(mFd); + } + } + + public static class NativeTcpSocket extends NativeSocket implements GenericTcpSocket { + public NativeTcpSocket(FileDescriptor fd) { + super(fd); + } + } + + public static class NativeUdpSocket extends NativeSocket implements GenericUdpSocket { + public NativeUdpSocket(FileDescriptor fd) { + super(fd); + } + + @Override + public void sendTo(byte[] data, InetAddress dstAddr, int port) throws Exception { + Os.sendto(mFd, data, 0, data.length, 0, dstAddr, port); + } + } + + public static class JavaUdpSocket implements GenericUdpSocket { + public final DatagramSocket mSocket; + + public JavaUdpSocket(InetAddress localAddr, int port) { + try { + mSocket = new DatagramSocket(port, localAddr); + mSocket.setSoTimeout(SOCK_TIMEOUT); + } catch (SocketException e) { + // Fail loudly if we can't set up sockets properly. And without the timeout, we + // could easily end up in an endless wait. + throw new RuntimeException(e); + } + } + + public JavaUdpSocket(InetAddress localAddr) { + try { + mSocket = new DatagramSocket(0, localAddr); + mSocket.setSoTimeout(SOCK_TIMEOUT); + } catch (SocketException e) { + // Fail loudly if we can't set up sockets properly. And without the timeout, we + // could easily end up in an endless wait. + throw new RuntimeException(e); + } + } + + @Override + public void send(byte[] data) throws Exception { + mSocket.send(new DatagramPacket(data, data.length)); + } + + @Override + public void sendTo(byte[] data, InetAddress dstAddr, int port) throws Exception { + mSocket.send(new DatagramPacket(data, data.length, dstAddr, port)); + } + + @Override + public int getPort() throws Exception { + return mSocket.getLocalPort(); + } + + @Override + public void close() throws Exception { + mSocket.close(); + } + + @Override + public byte[] receive() throws Exception { + DatagramPacket data = new DatagramPacket(new byte[DATA_BUFFER_LEN], DATA_BUFFER_LEN); + mSocket.receive(data); + return Arrays.copyOfRange(data.getData(), 0, data.getLength()); + } + + @Override + public void applyTransportModeTransform( + IpSecManager ism, int direction, IpSecTransform transform) throws Exception { + ism.applyTransportModeTransform(mSocket, direction, transform); + } + + @Override + public void removeTransportModeTransforms(IpSecManager ism) throws Exception { + ism.removeTransportModeTransforms(mSocket); + } + } + + public static class JavaTcpSocket implements GenericTcpSocket { + public final Socket mSocket; + + public JavaTcpSocket(Socket socket) { + mSocket = socket; + try { + mSocket.setSoTimeout(SOCK_TIMEOUT); + } catch (SocketException e) { + // Fail loudly if we can't set up sockets properly. And without the timeout, we + // could easily end up in an endless wait. + throw new RuntimeException(e); + } + } + + @Override + public void send(byte[] data) throws Exception { + mSocket.getOutputStream().write(data); + } + + @Override + public byte[] receive() throws Exception { + byte[] in = new byte[DATA_BUFFER_LEN]; + int bytesRead = mSocket.getInputStream().read(in); + return Arrays.copyOfRange(in, 0, bytesRead); + } + + @Override + public int getPort() throws Exception { + return mSocket.getLocalPort(); + } + + @Override + public void close() throws Exception { + mSocket.close(); + } + + @Override + public void applyTransportModeTransform( + IpSecManager ism, int direction, IpSecTransform transform) throws Exception { + ism.applyTransportModeTransform(mSocket, direction, transform); + } + + @Override + public void removeTransportModeTransforms(IpSecManager ism) throws Exception { + ism.removeTransportModeTransforms(mSocket); + } + } + + public static class SocketPair { + public final T mLeftSock; + public final T mRightSock; + + public SocketPair(T leftSock, T rightSock) { + mLeftSock = leftSock; + mRightSock = rightSock; + } + } + + protected static void applyTransformBidirectionally( + IpSecManager ism, IpSecTransform transform, GenericSocket socket) throws Exception { + for (int direction : DIRECTIONS) { + socket.applyTransportModeTransform(ism, direction, transform); + } + } + + public static SocketPair getNativeUdpSocketPair( + InetAddress localAddr, IpSecManager ism, IpSecTransform transform, boolean connected) + throws Exception { + int domain = getDomain(localAddr); + + NativeUdpSocket leftSock = new NativeUdpSocket( + Os.socket(domain, OsConstants.SOCK_DGRAM, OsConstants.IPPROTO_UDP)); + NativeUdpSocket rightSock = new NativeUdpSocket( + Os.socket(domain, OsConstants.SOCK_DGRAM, OsConstants.IPPROTO_UDP)); + + for (NativeUdpSocket sock : new NativeUdpSocket[] {leftSock, rightSock}) { + applyTransformBidirectionally(ism, transform, sock); + Os.bind(sock.mFd, localAddr, 0); + } + + if (connected) { + Os.connect(leftSock.mFd, localAddr, rightSock.getPort()); + Os.connect(rightSock.mFd, localAddr, leftSock.getPort()); + } + + return new SocketPair<>(leftSock, rightSock); + } + + public static SocketPair getNativeTcpSocketPair( + InetAddress localAddr, IpSecManager ism, IpSecTransform transform) throws Exception { + int domain = getDomain(localAddr); + + NativeTcpSocket server = new NativeTcpSocket( + Os.socket(domain, OsConstants.SOCK_STREAM, OsConstants.IPPROTO_TCP)); + NativeTcpSocket client = new NativeTcpSocket( + Os.socket(domain, OsConstants.SOCK_STREAM, OsConstants.IPPROTO_TCP)); + + Os.bind(server.mFd, localAddr, 0); + + applyTransformBidirectionally(ism, transform, server); + applyTransformBidirectionally(ism, transform, client); + + Os.listen(server.mFd, 10); + Os.connect(client.mFd, localAddr, server.getPort()); + NativeTcpSocket accepted = new NativeTcpSocket(Os.accept(server.mFd, null)); + + applyTransformBidirectionally(ism, transform, accepted); + server.close(); + + return new SocketPair<>(client, accepted); + } + + public static SocketPair getJavaUdpSocketPair( + InetAddress localAddr, IpSecManager ism, IpSecTransform transform, boolean connected) + throws Exception { + JavaUdpSocket leftSock = new JavaUdpSocket(localAddr); + JavaUdpSocket rightSock = new JavaUdpSocket(localAddr); + + applyTransformBidirectionally(ism, transform, leftSock); + applyTransformBidirectionally(ism, transform, rightSock); + + if (connected) { + leftSock.mSocket.connect(localAddr, rightSock.mSocket.getLocalPort()); + rightSock.mSocket.connect(localAddr, leftSock.mSocket.getLocalPort()); + } + + return new SocketPair<>(leftSock, rightSock); + } + + public static SocketPair getJavaTcpSocketPair( + InetAddress localAddr, IpSecManager ism, IpSecTransform transform) throws Exception { + JavaTcpSocket clientSock = new JavaTcpSocket(new Socket()); + ServerSocket serverSocket = new ServerSocket(); + serverSocket.bind(new InetSocketAddress(localAddr, 0)); + + // While technically the client socket does not need to be bound, the OpenJDK implementation + // of Socket only allocates an FD when bind() or connect() or other similar methods are + // called. So we call bind to force the FD creation, so that we can apply a transform to it + // prior to socket connect. + clientSock.mSocket.bind(new InetSocketAddress(localAddr, 0)); + + // IpSecService doesn't support serverSockets at the moment; workaround using FD + FileDescriptor serverFd = serverSocket.getImpl().getFD$(); + + applyTransformBidirectionally(ism, transform, new NativeTcpSocket(serverFd)); + applyTransformBidirectionally(ism, transform, clientSock); + + clientSock.mSocket.connect(new InetSocketAddress(localAddr, serverSocket.getLocalPort())); + JavaTcpSocket acceptedSock = new JavaTcpSocket(serverSocket.accept()); + + applyTransformBidirectionally(ism, transform, acceptedSock); + serverSocket.close(); + + return new SocketPair<>(clientSock, acceptedSock); + } + + private void checkSocketPair(GenericSocket left, GenericSocket right) throws Exception { + left.send(TEST_DATA); + assertArrayEquals(TEST_DATA, right.receive()); + + right.send(TEST_DATA); + assertArrayEquals(TEST_DATA, left.receive()); + + left.close(); + right.close(); + } + + private void checkUnconnectedUdpSocketPair( + GenericUdpSocket left, GenericUdpSocket right, InetAddress localAddr) throws Exception { + left.sendTo(TEST_DATA, localAddr, right.getPort()); + assertArrayEquals(TEST_DATA, right.receive()); + + right.sendTo(TEST_DATA, localAddr, left.getPort()); + assertArrayEquals(TEST_DATA, left.receive()); + + left.close(); + right.close(); + } + + protected static IpSecTransform buildIpSecTransform( + Context context, + IpSecManager.SecurityParameterIndex spi, + IpSecManager.UdpEncapsulationSocket encapSocket, + InetAddress remoteAddr) + throws Exception { + IpSecTransform.Builder builder = + new IpSecTransform.Builder(context) + .setEncryption(new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY)) + .setAuthentication( + new IpSecAlgorithm( + IpSecAlgorithm.AUTH_HMAC_SHA256, + AUTH_KEY, + AUTH_KEY.length * 4)); + + if (encapSocket != null) { + builder.setIpv4Encapsulation(encapSocket, encapSocket.getPort()); + } + + return builder.buildTransportModeTransform(remoteAddr, spi); + } + + private IpSecTransform buildDefaultTransform(InetAddress localAddr) throws Exception { + try (IpSecManager.SecurityParameterIndex spi = + mISM.allocateSecurityParameterIndex(localAddr)) { + return buildIpSecTransform(InstrumentationRegistry.getContext(), spi, null, localAddr); + } + } + + @Test + @AppModeFull(reason = "Socket cannot bind in instant app mode") + public void testJavaTcpSocketPair() throws Exception { + for (String addr : LOOPBACK_ADDRS) { + InetAddress local = InetAddress.getByName(addr); + try (IpSecTransform transform = buildDefaultTransform(local)) { + SocketPair sockets = getJavaTcpSocketPair(local, mISM, transform); + checkSocketPair(sockets.mLeftSock, sockets.mRightSock); + } + } + } + + @Test + @AppModeFull(reason = "Socket cannot bind in instant app mode") + public void testJavaUdpSocketPair() throws Exception { + for (String addr : LOOPBACK_ADDRS) { + InetAddress local = InetAddress.getByName(addr); + try (IpSecTransform transform = buildDefaultTransform(local)) { + SocketPair sockets = + getJavaUdpSocketPair(local, mISM, transform, true); + checkSocketPair(sockets.mLeftSock, sockets.mRightSock); + } + } + } + + @Test + @AppModeFull(reason = "Socket cannot bind in instant app mode") + public void testJavaUdpSocketPairUnconnected() throws Exception { + for (String addr : LOOPBACK_ADDRS) { + InetAddress local = InetAddress.getByName(addr); + try (IpSecTransform transform = buildDefaultTransform(local)) { + SocketPair sockets = + getJavaUdpSocketPair(local, mISM, transform, false); + checkUnconnectedUdpSocketPair(sockets.mLeftSock, sockets.mRightSock, local); + } + } + } + + @Test + @AppModeFull(reason = "Socket cannot bind in instant app mode") + public void testNativeTcpSocketPair() throws Exception { + for (String addr : LOOPBACK_ADDRS) { + InetAddress local = InetAddress.getByName(addr); + try (IpSecTransform transform = buildDefaultTransform(local)) { + SocketPair sockets = + getNativeTcpSocketPair(local, mISM, transform); + checkSocketPair(sockets.mLeftSock, sockets.mRightSock); + } + } + } + + @Test + @AppModeFull(reason = "Socket cannot bind in instant app mode") + public void testNativeUdpSocketPair() throws Exception { + for (String addr : LOOPBACK_ADDRS) { + InetAddress local = InetAddress.getByName(addr); + try (IpSecTransform transform = buildDefaultTransform(local)) { + SocketPair sockets = + getNativeUdpSocketPair(local, mISM, transform, true); + checkSocketPair(sockets.mLeftSock, sockets.mRightSock); + } + } + } + + @Test + @AppModeFull(reason = "Socket cannot bind in instant app mode") + public void testNativeUdpSocketPairUnconnected() throws Exception { + for (String addr : LOOPBACK_ADDRS) { + InetAddress local = InetAddress.getByName(addr); + try (IpSecTransform transform = buildDefaultTransform(local)) { + SocketPair sockets = + getNativeUdpSocketPair(local, mISM, transform, false); + checkUnconnectedUdpSocketPair(sockets.mLeftSock, sockets.mRightSock, local); + } + } + } +} diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java new file mode 100644 index 0000000000..355b496829 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java @@ -0,0 +1,1189 @@ +/* + * 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.cts; + +import static android.net.cts.PacketUtils.AES_CBC_BLK_SIZE; +import static android.net.cts.PacketUtils.AES_CBC_IV_LEN; +import static android.net.cts.PacketUtils.AES_GCM_BLK_SIZE; +import static android.net.cts.PacketUtils.AES_GCM_IV_LEN; +import static android.net.cts.PacketUtils.IP4_HDRLEN; +import static android.net.cts.PacketUtils.IP6_HDRLEN; +import static android.net.cts.PacketUtils.TCP_HDRLEN_WITH_TIMESTAMP_OPT; +import static android.net.cts.PacketUtils.UDP_HDRLEN; +import static android.system.OsConstants.IPPROTO_TCP; +import static android.system.OsConstants.IPPROTO_UDP; + +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.IpSecAlgorithm; +import android.net.IpSecManager; +import android.net.IpSecTransform; +import android.net.TrafficStats; +import android.platform.test.annotations.AppModeFull; +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@AppModeFull(reason = "Socket cannot bind in instant app mode") +public class IpSecManagerTest extends IpSecBaseTest { + + private static final String TAG = IpSecManagerTest.class.getSimpleName(); + + private static final InetAddress GOOGLE_DNS_4 = InetAddress.parseNumericAddress("8.8.8.8"); + private static final InetAddress GOOGLE_DNS_6 = + InetAddress.parseNumericAddress("2001:4860:4860::8888"); + + private static final InetAddress[] GOOGLE_DNS_LIST = + new InetAddress[] {GOOGLE_DNS_4, GOOGLE_DNS_6}; + + private static final int DROID_SPI = 0xD1201D; + private static final int MAX_PORT_BIND_ATTEMPTS = 10; + + private static final byte[] AEAD_KEY = getKey(288); + + /* + * Allocate a random SPI + * Allocate a specific SPI using previous randomly created SPI value + * Realloc the same SPI that was specifically created (expect SpiUnavailable) + * Close SPIs + */ + @Test + public void testAllocSpi() throws Exception { + for (InetAddress addr : GOOGLE_DNS_LIST) { + IpSecManager.SecurityParameterIndex randomSpi = null, droidSpi = null; + randomSpi = mISM.allocateSecurityParameterIndex(addr); + assertTrue( + "Failed to receive a valid SPI", + randomSpi.getSpi() != IpSecManager.INVALID_SECURITY_PARAMETER_INDEX); + + droidSpi = mISM.allocateSecurityParameterIndex(addr, DROID_SPI); + assertTrue("Failed to allocate specified SPI, " + DROID_SPI, + droidSpi.getSpi() == DROID_SPI); + + try { + mISM.allocateSecurityParameterIndex(addr, DROID_SPI); + fail("Duplicate SPI was allowed to be created"); + } catch (IpSecManager.SpiUnavailableException expected) { + // This is a success case because we expect a dupe SPI to throw + } + + randomSpi.close(); + droidSpi.close(); + } + } + + /** This function finds an available port */ + private static int findUnusedPort() throws Exception { + // Get an available port. + DatagramSocket s = new DatagramSocket(); + int port = s.getLocalPort(); + s.close(); + return port; + } + + private static FileDescriptor getBoundUdpSocket(InetAddress address) throws Exception { + FileDescriptor sock = + Os.socket(getDomain(address), OsConstants.SOCK_DGRAM, OsConstants.IPPROTO_UDP); + + for (int i = 0; i < MAX_PORT_BIND_ATTEMPTS; i++) { + try { + int port = findUnusedPort(); + Os.bind(sock, address, port); + break; + } catch (ErrnoException e) { + // Someone claimed the port since we called findUnusedPort. + if (e.errno == OsConstants.EADDRINUSE) { + if (i == MAX_PORT_BIND_ATTEMPTS - 1) { + + fail("Failed " + MAX_PORT_BIND_ATTEMPTS + " attempts to bind to a port"); + } + continue; + } + throw e.rethrowAsIOException(); + } + } + return sock; + } + + private void checkUnconnectedUdp(IpSecTransform transform, InetAddress local, int sendCount, + boolean useJavaSockets) throws Exception { + GenericUdpSocket sockLeft = null, sockRight = null; + if (useJavaSockets) { + SocketPair sockets = getJavaUdpSocketPair(local, mISM, transform, false); + sockLeft = sockets.mLeftSock; + sockRight = sockets.mRightSock; + } else { + SocketPair sockets = + getNativeUdpSocketPair(local, mISM, transform, false); + sockLeft = sockets.mLeftSock; + sockRight = sockets.mRightSock; + } + + for (int i = 0; i < sendCount; i++) { + byte[] in; + + sockLeft.sendTo(TEST_DATA, local, sockRight.getPort()); + in = sockRight.receive(); + assertArrayEquals("Left-to-right encrypted data did not match.", TEST_DATA, in); + + sockRight.sendTo(TEST_DATA, local, sockLeft.getPort()); + in = sockLeft.receive(); + assertArrayEquals("Right-to-left encrypted data did not match.", TEST_DATA, in); + } + + sockLeft.close(); + sockRight.close(); + } + + private void checkTcp(IpSecTransform transform, InetAddress local, int sendCount, + boolean useJavaSockets) throws Exception { + GenericTcpSocket client = null, accepted = null; + if (useJavaSockets) { + SocketPair sockets = getJavaTcpSocketPair(local, mISM, transform); + client = sockets.mLeftSock; + accepted = sockets.mRightSock; + } else { + SocketPair sockets = getNativeTcpSocketPair(local, mISM, transform); + client = sockets.mLeftSock; + accepted = sockets.mRightSock; + } + + // Wait for TCP handshake packets to be counted + StatsChecker.waitForNumPackets(3); // (SYN, SYN+ACK, ACK) + + // Reset StatsChecker, to ignore negotiation overhead. + StatsChecker.initStatsChecker(); + for (int i = 0; i < sendCount; i++) { + byte[] in; + + client.send(TEST_DATA); + in = accepted.receive(); + assertArrayEquals("Client-to-server encrypted data did not match.", TEST_DATA, in); + + // Allow for newest data + ack packets to be returned before sending next packet + // Also add the number of expected packets in each of the previous runs (4 per run) + StatsChecker.waitForNumPackets(2 + (4 * i)); + + accepted.send(TEST_DATA); + in = client.receive(); + assertArrayEquals("Server-to-client encrypted data did not match.", TEST_DATA, in); + + // Allow for all data + ack packets to be returned before sending next packet + // Also add the number of expected packets in each of the previous runs (4 per run) + StatsChecker.waitForNumPackets(4 * (i + 1)); + } + + // Transforms should not be removed from the sockets, otherwise FIN packets will be sent + // unencrypted. + // This test also unfortunately happens to rely on a nuance of the cleanup order. By + // keeping the policy on the socket, but removing the SA before lingering FIN packets + // are sent (at an undetermined later time), the FIN packets are dropped. Without this, + // we run into all kinds of headaches trying to test data accounting (unsolicited + // packets mysteriously appearing and messing up our counters) + // The right way to close sockets is to set SO_LINGER to ensure synchronous closure, + // closing the sockets, and then closing the transforms. See documentation for the + // Socket or FileDescriptor flavors of applyTransportModeTransform() in IpSecManager + // for more details. + + client.close(); + accepted.close(); + } + + /* + * Alloc outbound SPI + * Alloc inbound SPI + * Create transport mode transform + * open socket + * apply transform to socket + * send data on socket + * release transform + * send data (expect exception) + */ + @Test + public void testCreateTransform() throws Exception { + InetAddress localAddr = InetAddress.getByName(IPV4_LOOPBACK); + IpSecManager.SecurityParameterIndex spi = + mISM.allocateSecurityParameterIndex(localAddr); + + IpSecTransform transform = + new IpSecTransform.Builder(InstrumentationRegistry.getContext()) + .setEncryption(new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY)) + .setAuthentication( + new IpSecAlgorithm( + IpSecAlgorithm.AUTH_HMAC_SHA256, + AUTH_KEY, + AUTH_KEY.length * 8)) + .buildTransportModeTransform(localAddr, spi); + + final boolean [][] applyInApplyOut = { + {false, false}, {false, true}, {true, false}, {true,true}}; + final byte[] data = new String("Best test data ever!").getBytes("UTF-8"); + final DatagramPacket outPacket = new DatagramPacket(data, 0, data.length, localAddr, 0); + + byte[] in = new byte[data.length]; + DatagramPacket inPacket = new DatagramPacket(in, in.length); + DatagramSocket localSocket; + int localPort; + + for(boolean[] io : applyInApplyOut) { + boolean applyIn = io[0]; + boolean applyOut = io[1]; + // Bind localSocket to a random available port. + localSocket = new DatagramSocket(0); + localPort = localSocket.getLocalPort(); + localSocket.setSoTimeout(200); + outPacket.setPort(localPort); + if (applyIn) { + mISM.applyTransportModeTransform( + localSocket, IpSecManager.DIRECTION_IN, transform); + } + if (applyOut) { + mISM.applyTransportModeTransform( + localSocket, IpSecManager.DIRECTION_OUT, transform); + } + if (applyIn == applyOut) { + localSocket.send(outPacket); + localSocket.receive(inPacket); + assertTrue("Encapsulated data did not match.", + Arrays.equals(outPacket.getData(), inPacket.getData())); + mISM.removeTransportModeTransforms(localSocket); + localSocket.close(); + } else { + try { + localSocket.send(outPacket); + localSocket.receive(inPacket); + } catch (IOException e) { + continue; + } finally { + mISM.removeTransportModeTransforms(localSocket); + localSocket.close(); + } + // FIXME: This check is disabled because sockets currently receive data + // if there is a valid SA for decryption, even when the input policy is + // not applied to a socket. + // fail("Data IO should fail on asymmetrical transforms! + Input=" + // + applyIn + " Output=" + applyOut); + } + } + transform.close(); + } + + /** Snapshot of TrafficStats as of initStatsChecker call for later comparisons */ + private static class StatsChecker { + private static final double ERROR_MARGIN_BYTES = 1.05; + private static final double ERROR_MARGIN_PKTS = 1.05; + private static final int MAX_WAIT_TIME_MILLIS = 1000; + + private static long uidTxBytes; + private static long uidRxBytes; + private static long uidTxPackets; + private static long uidRxPackets; + + private static long ifaceTxBytes; + private static long ifaceRxBytes; + private static long ifaceTxPackets; + private static long ifaceRxPackets; + + /** + * This method counts the number of incoming packets, polling intermittently up to + * MAX_WAIT_TIME_MILLIS. + */ + private static void waitForNumPackets(int numPackets) throws Exception { + long uidTxDelta = 0; + long uidRxDelta = 0; + for (int i = 0; i < 100; i++) { + uidTxDelta = TrafficStats.getUidTxPackets(Os.getuid()) - uidTxPackets; + uidRxDelta = TrafficStats.getUidRxPackets(Os.getuid()) - uidRxPackets; + + // TODO: Check Rx packets as well once kernel security policy bug is fixed. + // (b/70635417) + if (uidTxDelta >= numPackets) { + return; + } + Thread.sleep(MAX_WAIT_TIME_MILLIS / 100); + } + fail( + "Not enough traffic was recorded to satisfy the provided conditions: wanted " + + numPackets + + ", got " + + uidTxDelta + + " tx and " + + uidRxDelta + + " rx packets"); + } + + private static void assertUidStatsDelta( + int expectedTxByteDelta, + int expectedTxPacketDelta, + int minRxByteDelta, + int maxRxByteDelta, + int expectedRxPacketDelta) { + long newUidTxBytes = TrafficStats.getUidTxBytes(Os.getuid()); + long newUidRxBytes = TrafficStats.getUidRxBytes(Os.getuid()); + long newUidTxPackets = TrafficStats.getUidTxPackets(Os.getuid()); + long newUidRxPackets = TrafficStats.getUidRxPackets(Os.getuid()); + + assertEquals(expectedTxByteDelta, newUidTxBytes - uidTxBytes); + assertTrue( + newUidRxBytes - uidRxBytes >= minRxByteDelta + && newUidRxBytes - uidRxBytes <= maxRxByteDelta); + assertEquals(expectedTxPacketDelta, newUidTxPackets - uidTxPackets); + assertEquals(expectedRxPacketDelta, newUidRxPackets - uidRxPackets); + } + + private static void assertIfaceStatsDelta( + int expectedTxByteDelta, + int expectedTxPacketDelta, + int expectedRxByteDelta, + int expectedRxPacketDelta) + throws IOException { + long newIfaceTxBytes = TrafficStats.getLoopbackTxBytes(); + long newIfaceRxBytes = TrafficStats.getLoopbackRxBytes(); + long newIfaceTxPackets = TrafficStats.getLoopbackTxPackets(); + long newIfaceRxPackets = TrafficStats.getLoopbackRxPackets(); + + // Check that iface stats are within an acceptable range; data might be sent + // on the local interface by other apps. + assertApproxEquals( + ifaceTxBytes, newIfaceTxBytes, expectedTxByteDelta, ERROR_MARGIN_BYTES); + assertApproxEquals( + ifaceRxBytes, newIfaceRxBytes, expectedRxByteDelta, ERROR_MARGIN_BYTES); + assertApproxEquals( + ifaceTxPackets, newIfaceTxPackets, expectedTxPacketDelta, ERROR_MARGIN_PKTS); + assertApproxEquals( + ifaceRxPackets, newIfaceRxPackets, expectedRxPacketDelta, ERROR_MARGIN_PKTS); + } + + private static void assertApproxEquals( + long oldStats, long newStats, int expectedDelta, double errorMargin) { + assertTrue(expectedDelta <= newStats - oldStats); + assertTrue((expectedDelta * errorMargin) > newStats - oldStats); + } + + private static void initStatsChecker() throws Exception { + uidTxBytes = TrafficStats.getUidTxBytes(Os.getuid()); + uidRxBytes = TrafficStats.getUidRxBytes(Os.getuid()); + uidTxPackets = TrafficStats.getUidTxPackets(Os.getuid()); + uidRxPackets = TrafficStats.getUidRxPackets(Os.getuid()); + + ifaceTxBytes = TrafficStats.getLoopbackTxBytes(); + ifaceRxBytes = TrafficStats.getLoopbackRxBytes(); + ifaceTxPackets = TrafficStats.getLoopbackTxPackets(); + ifaceRxPackets = TrafficStats.getLoopbackRxPackets(); + } + } + + private int getTruncLenBits(IpSecAlgorithm authOrAead) { + return authOrAead == null ? 0 : authOrAead.getTruncationLengthBits(); + } + + private int getIvLen(IpSecAlgorithm cryptOrAead) { + if (cryptOrAead == null) { return 0; } + + switch (cryptOrAead.getName()) { + case IpSecAlgorithm.CRYPT_AES_CBC: + return AES_CBC_IV_LEN; + case IpSecAlgorithm.AUTH_CRYPT_AES_GCM: + return AES_GCM_IV_LEN; + default: + throw new IllegalArgumentException( + "IV length unknown for algorithm" + cryptOrAead.getName()); + } + } + + private int getBlkSize(IpSecAlgorithm cryptOrAead) { + // RFC 4303, section 2.4 states that ciphertext plus pad_len, next_header fields must + // terminate on a 4-byte boundary. Thus, the minimum ciphertext block size is 4 bytes. + if (cryptOrAead == null) { return 4; } + + switch (cryptOrAead.getName()) { + case IpSecAlgorithm.CRYPT_AES_CBC: + return AES_CBC_BLK_SIZE; + case IpSecAlgorithm.AUTH_CRYPT_AES_GCM: + return AES_GCM_BLK_SIZE; + default: + throw new IllegalArgumentException( + "Blk size unknown for algorithm" + cryptOrAead.getName()); + } + } + + public void checkTransform( + int protocol, + String localAddress, + IpSecAlgorithm crypt, + IpSecAlgorithm auth, + IpSecAlgorithm aead, + boolean doUdpEncap, + int sendCount, + boolean useJavaSockets) + throws Exception { + StatsChecker.initStatsChecker(); + InetAddress local = InetAddress.getByName(localAddress); + + try (IpSecManager.UdpEncapsulationSocket encapSocket = mISM.openUdpEncapsulationSocket(); + IpSecManager.SecurityParameterIndex spi = + mISM.allocateSecurityParameterIndex(local)) { + + IpSecTransform.Builder transformBuilder = + new IpSecTransform.Builder(InstrumentationRegistry.getContext()); + if (crypt != null) { + transformBuilder.setEncryption(crypt); + } + if (auth != null) { + transformBuilder.setAuthentication(auth); + } + if (aead != null) { + transformBuilder.setAuthenticatedEncryption(aead); + } + + if (doUdpEncap) { + transformBuilder = + transformBuilder.setIpv4Encapsulation(encapSocket, encapSocket.getPort()); + } + + int ipHdrLen = local instanceof Inet6Address ? IP6_HDRLEN : IP4_HDRLEN; + int transportHdrLen = 0; + int udpEncapLen = doUdpEncap ? UDP_HDRLEN : 0; + + try (IpSecTransform transform = + transformBuilder.buildTransportModeTransform(local, spi)) { + if (protocol == IPPROTO_TCP) { + transportHdrLen = TCP_HDRLEN_WITH_TIMESTAMP_OPT; + checkTcp(transform, local, sendCount, useJavaSockets); + } else if (protocol == IPPROTO_UDP) { + transportHdrLen = UDP_HDRLEN; + + // TODO: Also check connected udp. + checkUnconnectedUdp(transform, local, sendCount, useJavaSockets); + } else { + throw new IllegalArgumentException("Invalid protocol"); + } + } + + checkStatsChecker( + protocol, + ipHdrLen, + transportHdrLen, + udpEncapLen, + sendCount, + getIvLen(crypt != null ? crypt : aead), + getBlkSize(crypt != null ? crypt : aead), + getTruncLenBits(auth != null ? auth : aead)); + } + } + + private void checkStatsChecker( + int protocol, + int ipHdrLen, + int transportHdrLen, + int udpEncapLen, + int sendCount, + int ivLen, + int blkSize, + int truncLenBits) + throws Exception { + + int innerPacketSize = TEST_DATA.length + transportHdrLen + ipHdrLen; + int outerPacketSize = + PacketUtils.calculateEspPacketSize( + TEST_DATA.length + transportHdrLen, ivLen, blkSize, truncLenBits) + + udpEncapLen + + ipHdrLen; + + int expectedOuterBytes = outerPacketSize * sendCount; + int expectedInnerBytes = innerPacketSize * sendCount; + int expectedPackets = sendCount; + + // Each run sends two packets, one in each direction. + sendCount *= 2; + expectedOuterBytes *= 2; + expectedInnerBytes *= 2; + expectedPackets *= 2; + + // Add TCP ACKs for data packets + if (protocol == IPPROTO_TCP) { + int encryptedTcpPktSize = + PacketUtils.calculateEspPacketSize( + TCP_HDRLEN_WITH_TIMESTAMP_OPT, ivLen, blkSize, truncLenBits); + + // Add data packet ACKs + expectedOuterBytes += (encryptedTcpPktSize + udpEncapLen + ipHdrLen) * (sendCount); + expectedInnerBytes += (TCP_HDRLEN_WITH_TIMESTAMP_OPT + ipHdrLen) * (sendCount); + expectedPackets += sendCount; + } + + StatsChecker.waitForNumPackets(expectedPackets); + + // eBPF only counts inner packets, whereas xt_qtaguid counts outer packets. Allow both + StatsChecker.assertUidStatsDelta( + expectedOuterBytes, + expectedPackets, + expectedInnerBytes, + expectedOuterBytes, + expectedPackets); + + // Unreliable at low numbers due to potential interference from other processes. + if (sendCount >= 1000) { + StatsChecker.assertIfaceStatsDelta( + expectedOuterBytes, expectedPackets, expectedOuterBytes, expectedPackets); + } + } + + private void checkIkePacket( + NativeUdpSocket wrappedEncapSocket, InetAddress localAddr) throws Exception { + StatsChecker.initStatsChecker(); + + try (NativeUdpSocket remoteSocket = new NativeUdpSocket(getBoundUdpSocket(localAddr))) { + + // Append IKE/ESP header - 4 bytes of SPI, 4 bytes of seq number, all zeroed out + // If the first four bytes are zero, assume non-ESP (IKE traffic) + byte[] dataWithEspHeader = new byte[TEST_DATA.length + 8]; + System.arraycopy(TEST_DATA, 0, dataWithEspHeader, 8, TEST_DATA.length); + + // Send the IKE packet from remoteSocket to wrappedEncapSocket. Since IKE packets + // are multiplexed over the socket, we expect them to appear on the encap socket + // (as opposed to being decrypted and received on the non-encap socket) + remoteSocket.sendTo(dataWithEspHeader, localAddr, wrappedEncapSocket.getPort()); + byte[] in = wrappedEncapSocket.receive(); + assertArrayEquals("Encapsulated data did not match.", dataWithEspHeader, in); + + // Also test that the IKE socket can send data out. + wrappedEncapSocket.sendTo(dataWithEspHeader, localAddr, remoteSocket.getPort()); + in = remoteSocket.receive(); + assertArrayEquals("Encapsulated data did not match.", dataWithEspHeader, in); + + // Calculate expected packet sizes. Always use IPv4 header, since our kernels only + // guarantee support of UDP encap on IPv4. + int expectedNumPkts = 2; + int expectedPacketSize = + expectedNumPkts * (dataWithEspHeader.length + UDP_HDRLEN + IP4_HDRLEN); + + StatsChecker.waitForNumPackets(expectedNumPkts); + StatsChecker.assertUidStatsDelta( + expectedPacketSize, + expectedNumPkts, + expectedPacketSize, + expectedPacketSize, + expectedNumPkts); + StatsChecker.assertIfaceStatsDelta( + expectedPacketSize, expectedNumPkts, expectedPacketSize, expectedNumPkts); + } + } + + @Test + public void testIkeOverUdpEncapSocket() throws Exception { + // IPv6 not supported for UDP-encap-ESP + InetAddress local = InetAddress.getByName(IPV4_LOOPBACK); + try (IpSecManager.UdpEncapsulationSocket encapSocket = mISM.openUdpEncapsulationSocket()) { + NativeUdpSocket wrappedEncapSocket = + new NativeUdpSocket(encapSocket.getFileDescriptor()); + checkIkePacket(wrappedEncapSocket, local); + + // Now try with a transform applied to a socket using this Encap socket + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96); + + try (IpSecManager.SecurityParameterIndex spi = + mISM.allocateSecurityParameterIndex(local); + IpSecTransform transform = + new IpSecTransform.Builder(InstrumentationRegistry.getContext()) + .setEncryption(crypt) + .setAuthentication(auth) + .setIpv4Encapsulation(encapSocket, encapSocket.getPort()) + .buildTransportModeTransform(local, spi); + JavaUdpSocket localSocket = new JavaUdpSocket(local)) { + applyTransformBidirectionally(mISM, transform, localSocket); + + checkIkePacket(wrappedEncapSocket, local); + } + } + } + + // TODO: Check IKE over ESP sockets (IPv4, IPv6) - does this need SOCK_RAW? + + /* TODO: Re-enable these when policy matcher works for reflected packets + * + * The issue here is that A sends to B, and everything is new; therefore PREROUTING counts + * correctly. But it appears that the security path is not cleared afterwards, thus when A + * sends an ACK back to B, the policy matcher flags it as a "IPSec" packet. See b/70635417 + */ + + // public void testInterfaceCountersTcp4() throws Exception { + // IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + // IpSecAlgorithm auth = new IpSecAlgorithm( + // IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96); + // checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, false, 1000); + // } + + // public void testInterfaceCountersTcp6() throws Exception { + // IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + // IpSecAlgorithm auth = new IpSecAlgorithm( + // IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96); + // checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, false, 1000); + // } + + // public void testInterfaceCountersTcp4UdpEncap() throws Exception { + // IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + // IpSecAlgorithm auth = + // new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96); + // checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, true, 1000); + // } + + @Test + public void testInterfaceCountersUdp4() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1000, false); + } + + @Test + public void testInterfaceCountersUdp6() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96); + checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1000, false); + } + + @Test + public void testInterfaceCountersUdp4UdpEncap() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1000, false); + } + + @Test + public void testAesCbcHmacMd5Tcp4() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true); + } + + @Test + public void testAesCbcHmacMd5Tcp6() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96); + checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false); + checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true); + } + + @Test + public void testAesCbcHmacMd5Udp4() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true); + } + + @Test + public void testAesCbcHmacMd5Udp6() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96); + checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false); + checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true); + } + + @Test + public void testAesCbcHmacSha1Tcp4() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA1, getKey(160), 96); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true); + } + + @Test + public void testAesCbcHmacSha1Tcp6() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA1, getKey(160), 96); + checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false); + checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true); + } + + @Test + public void testAesCbcHmacSha1Udp4() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA1, getKey(160), 96); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true); + } + + @Test + public void testAesCbcHmacSha1Udp6() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA1, getKey(160), 96); + checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false); + checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true); + } + + @Test + public void testAesCbcHmacSha256Tcp4() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true); + } + + @Test + public void testAesCbcHmacSha256Tcp6() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128); + checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false); + checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true); + } + + @Test + public void testAesCbcHmacSha256Udp4() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true); + } + + @Test + public void testAesCbcHmacSha256Udp6() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128); + checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false); + checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true); + } + + @Test + public void testAesCbcHmacSha384Tcp4() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA384, getKey(384), 192); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true); + } + + @Test + public void testAesCbcHmacSha384Tcp6() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA384, getKey(384), 192); + checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false); + checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true); + } + + @Test + public void testAesCbcHmacSha384Udp4() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA384, getKey(384), 192); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true); + } + + @Test + public void testAesCbcHmacSha384Udp6() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA384, getKey(384), 192); + checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false); + checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true); + } + + @Test + public void testAesCbcHmacSha512Tcp4() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA512, getKey(512), 256); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true); + } + + @Test + public void testAesCbcHmacSha512Tcp6() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA512, getKey(512), 256); + checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false); + checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true); + } + + @Test + public void testAesCbcHmacSha512Udp4() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA512, getKey(512), 256); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true); + } + + @Test + public void testAesCbcHmacSha512Udp6() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA512, getKey(512), 256); + checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false); + checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true); + } + + @Test + public void testAesGcm64Tcp4() throws Exception { + IpSecAlgorithm authCrypt = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 64); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true); + } + + @Test + public void testAesGcm64Tcp6() throws Exception { + IpSecAlgorithm authCrypt = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 64); + checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false); + checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true); + } + + @Test + public void testAesGcm64Udp4() throws Exception { + IpSecAlgorithm authCrypt = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 64); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true); + } + + @Test + public void testAesGcm64Udp6() throws Exception { + IpSecAlgorithm authCrypt = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 64); + checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false); + checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true); + } + + @Test + public void testAesGcm96Tcp4() throws Exception { + IpSecAlgorithm authCrypt = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 96); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true); + } + + @Test + public void testAesGcm96Tcp6() throws Exception { + IpSecAlgorithm authCrypt = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 96); + checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false); + checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true); + } + + @Test + public void testAesGcm96Udp4() throws Exception { + IpSecAlgorithm authCrypt = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 96); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true); + } + + @Test + public void testAesGcm96Udp6() throws Exception { + IpSecAlgorithm authCrypt = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 96); + checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false); + checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true); + } + + @Test + public void testAesGcm128Tcp4() throws Exception { + IpSecAlgorithm authCrypt = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true); + } + + @Test + public void testAesGcm128Tcp6() throws Exception { + IpSecAlgorithm authCrypt = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128); + checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false); + checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true); + } + + @Test + public void testAesGcm128Udp4() throws Exception { + IpSecAlgorithm authCrypt = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true); + } + + @Test + public void testAesGcm128Udp6() throws Exception { + IpSecAlgorithm authCrypt = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128); + checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false); + checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true); + } + + @Test + public void testAesCbcHmacMd5Tcp4UdpEncap() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true); + } + + @Test + public void testAesCbcHmacMd5Udp4UdpEncap() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_MD5, getKey(128), 96); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true); + } + + @Test + public void testAesCbcHmacSha1Tcp4UdpEncap() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA1, getKey(160), 96); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true); + } + + @Test + public void testAesCbcHmacSha1Udp4UdpEncap() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA1, getKey(160), 96); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true); + } + + @Test + public void testAesCbcHmacSha256Tcp4UdpEncap() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true); + } + + @Test + public void testAesCbcHmacSha256Udp4UdpEncap() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true); + } + + @Test + public void testAesCbcHmacSha384Tcp4UdpEncap() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA384, getKey(384), 192); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true); + } + + @Test + public void testAesCbcHmacSha384Udp4UdpEncap() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA384, getKey(384), 192); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true); + } + + @Test + public void testAesCbcHmacSha512Tcp4UdpEncap() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA512, getKey(512), 256); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true); + } + + @Test + public void testAesCbcHmacSha512Udp4UdpEncap() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA512, getKey(512), 256); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true); + } + + @Test + public void testAesGcm64Tcp4UdpEncap() throws Exception { + IpSecAlgorithm authCrypt = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 64); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true); + } + + @Test + public void testAesGcm64Udp4UdpEncap() throws Exception { + IpSecAlgorithm authCrypt = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 64); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true); + } + + @Test + public void testAesGcm96Tcp4UdpEncap() throws Exception { + IpSecAlgorithm authCrypt = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 96); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true); + } + + @Test + public void testAesGcm96Udp4UdpEncap() throws Exception { + IpSecAlgorithm authCrypt = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 96); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true); + } + + @Test + public void testAesGcm128Tcp4UdpEncap() throws Exception { + IpSecAlgorithm authCrypt = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true); + } + + @Test + public void testAesGcm128Udp4UdpEncap() throws Exception { + IpSecAlgorithm authCrypt = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true); + } + + @Test + public void testCryptUdp4() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, null, null, false, 1, false); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, null, null, false, 1, true); + } + + @Test + public void testAuthUdp4() throws Exception { + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, auth, null, false, 1, false); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, auth, null, false, 1, true); + } + + @Test + public void testCryptUdp6() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, null, null, false, 1, false); + checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, null, null, false, 1, true); + } + + @Test + public void testAuthUdp6() throws Exception { + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128); + checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, auth, null, false, 1, false); + checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, auth, null, false, 1, true); + } + + @Test + public void testCryptTcp4() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, null, null, false, 1, false); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, null, null, false, 1, true); + } + + @Test + public void testAuthTcp4() throws Exception { + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, auth, null, false, 1, false); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, auth, null, false, 1, true); + } + + @Test + public void testCryptTcp6() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, null, null, false, 1, false); + checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, null, null, false, 1, true); + } + + @Test + public void testAuthTcp6() throws Exception { + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128); + checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, auth, null, false, 1, false); + checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, auth, null, false, 1, true); + } + + @Test + public void testCryptUdp4UdpEncap() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, null, null, true, 1, false); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, null, null, true, 1, true); + } + + @Test + public void testAuthUdp4UdpEncap() throws Exception { + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, auth, null, true, 1, false); + checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, auth, null, true, 1, true); + } + + @Test + public void testCryptTcp4UdpEncap() throws Exception { + IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, null, null, true, 1, false); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, null, null, true, 1, true); + } + + @Test + public void testAuthTcp4UdpEncap() throws Exception { + IpSecAlgorithm auth = new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, getKey(256), 128); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, auth, null, true, 1, false); + checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, auth, null, true, 1, true); + } + + @Test + public void testOpenUdpEncapSocketSpecificPort() throws Exception { + IpSecManager.UdpEncapsulationSocket encapSocket = null; + int port = -1; + for (int i = 0; i < MAX_PORT_BIND_ATTEMPTS; i++) { + try { + port = findUnusedPort(); + encapSocket = mISM.openUdpEncapsulationSocket(port); + break; + } catch (ErrnoException e) { + if (e.errno == OsConstants.EADDRINUSE) { + // Someone claimed the port since we called findUnusedPort. + continue; + } + throw e; + } finally { + if (encapSocket != null) { + encapSocket.close(); + } + } + } + + if (encapSocket == null) { + fail("Failed " + MAX_PORT_BIND_ATTEMPTS + " attempts to bind to a port"); + } + + assertTrue("Returned invalid port", encapSocket.getPort() == port); + } + + @Test + public void testOpenUdpEncapSocketRandomPort() throws Exception { + try (IpSecManager.UdpEncapsulationSocket encapSocket = mISM.openUdpEncapsulationSocket()) { + assertTrue("Returned invalid port", encapSocket.getPort() != 0); + } + } +} diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java new file mode 100644 index 0000000000..ae38faa124 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java @@ -0,0 +1,899 @@ +/* + * 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.cts; + +import static android.app.AppOpsManager.OP_MANAGE_IPSEC_TUNNELS; +import static android.net.IpSecManager.UdpEncapsulationSocket; +import static android.net.cts.PacketUtils.AES_CBC_BLK_SIZE; +import static android.net.cts.PacketUtils.AES_CBC_IV_LEN; +import static android.net.cts.PacketUtils.BytePayload; +import static android.net.cts.PacketUtils.EspHeader; +import static android.net.cts.PacketUtils.IP4_HDRLEN; +import static android.net.cts.PacketUtils.IP6_HDRLEN; +import static android.net.cts.PacketUtils.IpHeader; +import static android.net.cts.PacketUtils.UDP_HDRLEN; +import static android.net.cts.PacketUtils.UdpHeader; +import static android.net.cts.PacketUtils.getIpHeader; +import static android.net.cts.util.CtsNetUtils.TestNetworkCallback; +import static android.system.OsConstants.AF_INET; +import static android.system.OsConstants.AF_INET6; + +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.fail; +import static org.junit.Assume.assumeTrue; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.IpSecAlgorithm; +import android.net.IpSecManager; +import android.net.IpSecTransform; +import android.net.LinkAddress; +import android.net.Network; +import android.net.TestNetworkInterface; +import android.net.TestNetworkManager; +import android.net.cts.PacketUtils.Payload; +import android.net.cts.util.CtsNetUtils; +import android.os.ParcelFileDescriptor; +import android.platform.test.annotations.AppModeFull; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.NetworkInterface; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@AppModeFull(reason = "MANAGE_TEST_NETWORKS permission can't be granted to instant apps") +public class IpSecManagerTunnelTest extends IpSecBaseTest { + private static final String TAG = IpSecManagerTunnelTest.class.getSimpleName(); + + private static final InetAddress LOCAL_OUTER_4 = InetAddress.parseNumericAddress("192.0.2.1"); + private static final InetAddress REMOTE_OUTER_4 = InetAddress.parseNumericAddress("192.0.2.2"); + private static final InetAddress LOCAL_OUTER_6 = + InetAddress.parseNumericAddress("2001:db8:1::1"); + private static final InetAddress REMOTE_OUTER_6 = + InetAddress.parseNumericAddress("2001:db8:1::2"); + + private static final InetAddress LOCAL_INNER_4 = + InetAddress.parseNumericAddress("198.51.100.1"); + private static final InetAddress REMOTE_INNER_4 = + InetAddress.parseNumericAddress("198.51.100.2"); + private static final InetAddress LOCAL_INNER_6 = + InetAddress.parseNumericAddress("2001:db8:2::1"); + private static final InetAddress REMOTE_INNER_6 = + InetAddress.parseNumericAddress("2001:db8:2::2"); + + private static final int IP4_PREFIX_LEN = 32; + private static final int IP6_PREFIX_LEN = 128; + + private static final int TIMEOUT_MS = 500; + + // Static state to reduce setup/teardown + private static ConnectivityManager sCM; + private static TestNetworkManager sTNM; + private static ParcelFileDescriptor sTunFd; + private static TestNetworkCallback sTunNetworkCallback; + private static Network sTunNetwork; + private static TunUtils sTunUtils; + + private static Context sContext = InstrumentationRegistry.getContext(); + private static final CtsNetUtils mCtsNetUtils = new CtsNetUtils(sContext); + + @BeforeClass + public static void setUpBeforeClass() throws Exception { + InstrumentationRegistry.getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity(); + sCM = (ConnectivityManager) sContext.getSystemService(Context.CONNECTIVITY_SERVICE); + sTNM = (TestNetworkManager) sContext.getSystemService(Context.TEST_NETWORK_SERVICE); + + // Under normal circumstances, the MANAGE_IPSEC_TUNNELS appop would be auto-granted, and + // a standard permission is insufficient. So we shell out the appop, to give us the + // right appop permissions. + mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, true); + + TestNetworkInterface testIface = + sTNM.createTunInterface( + new LinkAddress[] { + new LinkAddress(LOCAL_OUTER_4, IP4_PREFIX_LEN), + new LinkAddress(LOCAL_OUTER_6, IP6_PREFIX_LEN) + }); + + sTunFd = testIface.getFileDescriptor(); + sTunNetworkCallback = mCtsNetUtils.setupAndGetTestNetwork(testIface.getInterfaceName()); + sTunNetworkCallback.waitForAvailable(); + sTunNetwork = sTunNetworkCallback.currentNetwork; + + sTunUtils = new TunUtils(sTunFd); + } + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + + // Set to true before every run; some tests flip this. + mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, true); + + // Clear sTunUtils state + sTunUtils.reset(); + } + + @AfterClass + public static void tearDownAfterClass() throws Exception { + mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, false); + + sCM.unregisterNetworkCallback(sTunNetworkCallback); + + sTNM.teardownTestNetwork(sTunNetwork); + sTunFd.close(); + + InstrumentationRegistry.getInstrumentation() + .getUiAutomation() + .dropShellPermissionIdentity(); + } + + @Test + public void testSecurityExceptionCreateTunnelInterfaceWithoutAppop() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + // Ensure we don't have the appop. Permission is not requested in the Manifest + mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, false); + + // Security exceptions are thrown regardless of IPv4/IPv6. Just test one + try { + mISM.createIpSecTunnelInterface(LOCAL_INNER_6, REMOTE_INNER_6, sTunNetwork); + fail("Did not throw SecurityException for Tunnel creation without appop"); + } catch (SecurityException expected) { + } + } + + @Test + public void testSecurityExceptionBuildTunnelTransformWithoutAppop() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + + // Ensure we don't have the appop. Permission is not requested in the Manifest + mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, false); + + // Security exceptions are thrown regardless of IPv4/IPv6. Just test one + try (IpSecManager.SecurityParameterIndex spi = + mISM.allocateSecurityParameterIndex(LOCAL_INNER_4); + IpSecTransform transform = + new IpSecTransform.Builder(sContext) + .buildTunnelModeTransform(REMOTE_INNER_4, spi)) { + fail("Did not throw SecurityException for Transform creation without appop"); + } catch (SecurityException expected) { + } + } + + /* Test runnables for callbacks after IPsec tunnels are set up. */ + private abstract class IpSecTunnelTestRunnable { + /** + * Runs the test code, and returns the inner socket port, if any. + * + * @param ipsecNetwork The IPsec Interface based Network for binding sockets on + * @return the integer port of the inner socket if outbound, or 0 if inbound + * IpSecTunnelTestRunnable + * @throws Exception if any part of the test failed. + */ + public abstract int run(Network ipsecNetwork) throws Exception; + } + + private int getPacketSize( + int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode) { + int expectedPacketSize = TEST_DATA.length + UDP_HDRLEN; + + // Inner Transport mode packet size + if (transportInTunnelMode) { + expectedPacketSize = + PacketUtils.calculateEspPacketSize( + expectedPacketSize, + AES_CBC_IV_LEN, + AES_CBC_BLK_SIZE, + AUTH_KEY.length * 4); + } + + // Inner IP Header + expectedPacketSize += innerFamily == AF_INET ? IP4_HDRLEN : IP6_HDRLEN; + + // Tunnel mode transform size + expectedPacketSize = + PacketUtils.calculateEspPacketSize( + expectedPacketSize, AES_CBC_IV_LEN, AES_CBC_BLK_SIZE, AUTH_KEY.length * 4); + + // UDP encap size + expectedPacketSize += useEncap ? UDP_HDRLEN : 0; + + // Outer IP Header + expectedPacketSize += outerFamily == AF_INET ? IP4_HDRLEN : IP6_HDRLEN; + + return expectedPacketSize; + } + + private interface IpSecTunnelTestRunnableFactory { + IpSecTunnelTestRunnable getIpSecTunnelTestRunnable( + boolean transportInTunnelMode, + int spi, + InetAddress localInner, + InetAddress remoteInner, + InetAddress localOuter, + InetAddress remoteOuter, + IpSecTransform inTransportTransform, + IpSecTransform outTransportTransform, + int encapPort, + int innerSocketPort, + int expectedPacketSize) + throws Exception; + } + + private class OutputIpSecTunnelTestRunnableFactory implements IpSecTunnelTestRunnableFactory { + public IpSecTunnelTestRunnable getIpSecTunnelTestRunnable( + boolean transportInTunnelMode, + int spi, + InetAddress localInner, + InetAddress remoteInner, + InetAddress localOuter, + InetAddress remoteOuter, + IpSecTransform inTransportTransform, + IpSecTransform outTransportTransform, + int encapPort, + int unusedInnerSocketPort, + int expectedPacketSize) { + return new IpSecTunnelTestRunnable() { + @Override + public int run(Network ipsecNetwork) throws Exception { + // Build a socket and send traffic + JavaUdpSocket socket = new JavaUdpSocket(localInner); + ipsecNetwork.bindSocket(socket.mSocket); + int innerSocketPort = socket.getPort(); + + // For Transport-In-Tunnel mode, apply transform to socket + if (transportInTunnelMode) { + mISM.applyTransportModeTransform( + socket.mSocket, IpSecManager.DIRECTION_IN, inTransportTransform); + mISM.applyTransportModeTransform( + socket.mSocket, IpSecManager.DIRECTION_OUT, outTransportTransform); + } + + socket.sendTo(TEST_DATA, remoteInner, socket.getPort()); + + // Verify that an encrypted packet is sent. As of right now, checking encrypted + // body is not possible, due to the test not knowing some of the fields of the + // inner IP header (flow label, flags, etc) + sTunUtils.awaitEspPacketNoPlaintext( + spi, TEST_DATA, encapPort != 0, expectedPacketSize); + + socket.close(); + + return innerSocketPort; + } + }; + } + } + + private class InputReflectedIpSecTunnelTestRunnableFactory + implements IpSecTunnelTestRunnableFactory { + public IpSecTunnelTestRunnable getIpSecTunnelTestRunnable( + boolean transportInTunnelMode, + int spi, + InetAddress localInner, + InetAddress remoteInner, + InetAddress localOuter, + InetAddress remoteOuter, + IpSecTransform inTransportTransform, + IpSecTransform outTransportTransform, + int encapPort, + int innerSocketPort, + int expectedPacketSize) + throws Exception { + return new IpSecTunnelTestRunnable() { + @Override + public int run(Network ipsecNetwork) throws Exception { + // Build a socket and receive traffic + JavaUdpSocket socket = new JavaUdpSocket(localInner, innerSocketPort); + ipsecNetwork.bindSocket(socket.mSocket); + + // For Transport-In-Tunnel mode, apply transform to socket + if (transportInTunnelMode) { + mISM.applyTransportModeTransform( + socket.mSocket, IpSecManager.DIRECTION_IN, outTransportTransform); + mISM.applyTransportModeTransform( + socket.mSocket, IpSecManager.DIRECTION_OUT, inTransportTransform); + } + + sTunUtils.reflectPackets(); + + // Receive packet from socket, and validate that the payload is correct + receiveAndValidatePacket(socket); + + socket.close(); + + return 0; + } + }; + } + } + + private class InputPacketGeneratorIpSecTunnelTestRunnableFactory + implements IpSecTunnelTestRunnableFactory { + public IpSecTunnelTestRunnable getIpSecTunnelTestRunnable( + boolean transportInTunnelMode, + int spi, + InetAddress localInner, + InetAddress remoteInner, + InetAddress localOuter, + InetAddress remoteOuter, + IpSecTransform inTransportTransform, + IpSecTransform outTransportTransform, + int encapPort, + int innerSocketPort, + int expectedPacketSize) + throws Exception { + return new IpSecTunnelTestRunnable() { + @Override + public int run(Network ipsecNetwork) throws Exception { + // Build a socket and receive traffic + JavaUdpSocket socket = new JavaUdpSocket(localInner); + ipsecNetwork.bindSocket(socket.mSocket); + + // For Transport-In-Tunnel mode, apply transform to socket + if (transportInTunnelMode) { + mISM.applyTransportModeTransform( + socket.mSocket, IpSecManager.DIRECTION_IN, outTransportTransform); + mISM.applyTransportModeTransform( + socket.mSocket, IpSecManager.DIRECTION_OUT, inTransportTransform); + } + + byte[] pkt; + if (transportInTunnelMode) { + pkt = + getTransportInTunnelModePacket( + spi, + spi, + remoteInner, + localInner, + remoteOuter, + localOuter, + socket.getPort(), + encapPort); + } else { + pkt = + getTunnelModePacket( + spi, + remoteInner, + localInner, + remoteOuter, + localOuter, + socket.getPort(), + encapPort); + } + sTunUtils.injectPacket(pkt); + + // Receive packet from socket, and validate + receiveAndValidatePacket(socket); + + socket.close(); + + return 0; + } + }; + } + } + + private void checkTunnelOutput( + int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode) + throws Exception { + checkTunnel( + innerFamily, + outerFamily, + useEncap, + transportInTunnelMode, + new OutputIpSecTunnelTestRunnableFactory()); + } + + private void checkTunnelInput( + int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode) + throws Exception { + checkTunnel( + innerFamily, + outerFamily, + useEncap, + transportInTunnelMode, + new InputPacketGeneratorIpSecTunnelTestRunnableFactory()); + } + + /** + * Validates that the kernel can talk to itself. + * + *

This test takes an outbound IPsec packet, reflects it (by flipping IP src/dst), and + * injects it back into the TUN. This test then verifies that a packet with the correct payload + * is found on the specified socket/port. + */ + public void checkTunnelReflected( + int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode) + throws Exception { + InetAddress localInner = innerFamily == AF_INET ? LOCAL_INNER_4 : LOCAL_INNER_6; + InetAddress remoteInner = innerFamily == AF_INET ? REMOTE_INNER_4 : REMOTE_INNER_6; + + InetAddress localOuter = outerFamily == AF_INET ? LOCAL_OUTER_4 : LOCAL_OUTER_6; + InetAddress remoteOuter = outerFamily == AF_INET ? REMOTE_OUTER_4 : REMOTE_OUTER_6; + + // Preselect both SPI and encap port, to be used for both inbound and outbound tunnels. + int spi = getRandomSpi(localOuter, remoteOuter); + int expectedPacketSize = + getPacketSize(innerFamily, outerFamily, useEncap, transportInTunnelMode); + + try (IpSecManager.SecurityParameterIndex inTransportSpi = + mISM.allocateSecurityParameterIndex(localInner, spi); + IpSecManager.SecurityParameterIndex outTransportSpi = + mISM.allocateSecurityParameterIndex(remoteInner, spi); + IpSecTransform inTransportTransform = + buildIpSecTransform(sContext, inTransportSpi, null, remoteInner); + IpSecTransform outTransportTransform = + buildIpSecTransform(sContext, outTransportSpi, null, localInner); + UdpEncapsulationSocket encapSocket = mISM.openUdpEncapsulationSocket()) { + + // Run output direction tests + IpSecTunnelTestRunnable outputIpSecTunnelTestRunnable = + new OutputIpSecTunnelTestRunnableFactory() + .getIpSecTunnelTestRunnable( + transportInTunnelMode, + spi, + localInner, + remoteInner, + localOuter, + remoteOuter, + inTransportTransform, + outTransportTransform, + useEncap ? encapSocket.getPort() : 0, + 0, + expectedPacketSize); + int innerSocketPort = + buildTunnelNetworkAndRunTests( + localInner, + remoteInner, + localOuter, + remoteOuter, + spi, + useEncap ? encapSocket : null, + outputIpSecTunnelTestRunnable); + + // Input direction tests, with matching inner socket ports. + IpSecTunnelTestRunnable inputIpSecTunnelTestRunnable = + new InputReflectedIpSecTunnelTestRunnableFactory() + .getIpSecTunnelTestRunnable( + transportInTunnelMode, + spi, + remoteInner, + localInner, + localOuter, + remoteOuter, + inTransportTransform, + outTransportTransform, + useEncap ? encapSocket.getPort() : 0, + innerSocketPort, + expectedPacketSize); + buildTunnelNetworkAndRunTests( + remoteInner, + localInner, + localOuter, + remoteOuter, + spi, + useEncap ? encapSocket : null, + inputIpSecTunnelTestRunnable); + } + } + + public void checkTunnel( + int innerFamily, + int outerFamily, + boolean useEncap, + boolean transportInTunnelMode, + IpSecTunnelTestRunnableFactory factory) + throws Exception { + + InetAddress localInner = innerFamily == AF_INET ? LOCAL_INNER_4 : LOCAL_INNER_6; + InetAddress remoteInner = innerFamily == AF_INET ? REMOTE_INNER_4 : REMOTE_INNER_6; + + InetAddress localOuter = outerFamily == AF_INET ? LOCAL_OUTER_4 : LOCAL_OUTER_6; + InetAddress remoteOuter = outerFamily == AF_INET ? REMOTE_OUTER_4 : REMOTE_OUTER_6; + + // Preselect both SPI and encap port, to be used for both inbound and outbound tunnels. + // Re-uses the same SPI to ensure that even in cases of symmetric SPIs shared across tunnel + // and transport mode, packets are encrypted/decrypted properly based on the src/dst. + int spi = getRandomSpi(localOuter, remoteOuter); + int expectedPacketSize = + getPacketSize(innerFamily, outerFamily, useEncap, transportInTunnelMode); + + try (IpSecManager.SecurityParameterIndex inTransportSpi = + mISM.allocateSecurityParameterIndex(localInner, spi); + IpSecManager.SecurityParameterIndex outTransportSpi = + mISM.allocateSecurityParameterIndex(remoteInner, spi); + IpSecTransform inTransportTransform = + buildIpSecTransform(sContext, inTransportSpi, null, remoteInner); + IpSecTransform outTransportTransform = + buildIpSecTransform(sContext, outTransportSpi, null, localInner); + UdpEncapsulationSocket encapSocket = mISM.openUdpEncapsulationSocket()) { + + buildTunnelNetworkAndRunTests( + localInner, + remoteInner, + localOuter, + remoteOuter, + spi, + useEncap ? encapSocket : null, + factory.getIpSecTunnelTestRunnable( + transportInTunnelMode, + spi, + localInner, + remoteInner, + localOuter, + remoteOuter, + inTransportTransform, + outTransportTransform, + useEncap ? encapSocket.getPort() : 0, + 0, + expectedPacketSize)); + } + } + + private int buildTunnelNetworkAndRunTests( + InetAddress localInner, + InetAddress remoteInner, + InetAddress localOuter, + InetAddress remoteOuter, + int spi, + UdpEncapsulationSocket encapSocket, + IpSecTunnelTestRunnable test) + throws Exception { + int innerPrefixLen = localInner instanceof Inet6Address ? IP6_PREFIX_LEN : IP4_PREFIX_LEN; + TestNetworkCallback testNetworkCb = null; + int innerSocketPort; + + try (IpSecManager.SecurityParameterIndex inSpi = + mISM.allocateSecurityParameterIndex(localOuter, spi); + IpSecManager.SecurityParameterIndex outSpi = + mISM.allocateSecurityParameterIndex(remoteOuter, spi); + IpSecManager.IpSecTunnelInterface tunnelIface = + mISM.createIpSecTunnelInterface(localOuter, remoteOuter, sTunNetwork)) { + // Build the test network + tunnelIface.addAddress(localInner, innerPrefixLen); + testNetworkCb = mCtsNetUtils.setupAndGetTestNetwork(tunnelIface.getInterfaceName()); + testNetworkCb.waitForAvailable(); + Network testNetwork = testNetworkCb.currentNetwork; + + // Check interface was created + assertNotNull(NetworkInterface.getByName(tunnelIface.getInterfaceName())); + + // Verify address was added + final NetworkInterface netIface = NetworkInterface.getByInetAddress(localInner); + assertNotNull(netIface); + assertEquals(tunnelIface.getInterfaceName(), netIface.getDisplayName()); + + // Configure Transform parameters + IpSecTransform.Builder transformBuilder = new IpSecTransform.Builder(sContext); + transformBuilder.setEncryption( + new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY)); + transformBuilder.setAuthentication( + new IpSecAlgorithm( + IpSecAlgorithm.AUTH_HMAC_SHA256, AUTH_KEY, AUTH_KEY.length * 4)); + + if (encapSocket != null) { + transformBuilder.setIpv4Encapsulation(encapSocket, encapSocket.getPort()); + } + + // Apply transform and check that traffic is properly encrypted + try (IpSecTransform inTransform = + transformBuilder.buildTunnelModeTransform(remoteOuter, inSpi); + IpSecTransform outTransform = + transformBuilder.buildTunnelModeTransform(localOuter, outSpi)) { + mISM.applyTunnelModeTransform(tunnelIface, IpSecManager.DIRECTION_IN, inTransform); + mISM.applyTunnelModeTransform( + tunnelIface, IpSecManager.DIRECTION_OUT, outTransform); + + innerSocketPort = test.run(testNetwork); + } + + // Teardown the test network + sTNM.teardownTestNetwork(testNetwork); + + // Remove addresses and check that interface is still present, but fails lookup-by-addr + tunnelIface.removeAddress(localInner, innerPrefixLen); + assertNotNull(NetworkInterface.getByName(tunnelIface.getInterfaceName())); + assertNull(NetworkInterface.getByInetAddress(localInner)); + + // Check interface was cleaned up + tunnelIface.close(); + assertNull(NetworkInterface.getByName(tunnelIface.getInterfaceName())); + } finally { + if (testNetworkCb != null) { + sCM.unregisterNetworkCallback(testNetworkCb); + } + } + + return innerSocketPort; + } + + private static void receiveAndValidatePacket(JavaUdpSocket socket) throws Exception { + byte[] socketResponseBytes = socket.receive(); + assertArrayEquals(TEST_DATA, socketResponseBytes); + } + + private int getRandomSpi(InetAddress localOuter, InetAddress remoteOuter) throws Exception { + // Try to allocate both in and out SPIs using the same requested SPI value. + try (IpSecManager.SecurityParameterIndex inSpi = + mISM.allocateSecurityParameterIndex(localOuter); + IpSecManager.SecurityParameterIndex outSpi = + mISM.allocateSecurityParameterIndex(remoteOuter, inSpi.getSpi()); ) { + return inSpi.getSpi(); + } + } + + private EspHeader buildTransportModeEspPacket( + int spi, InetAddress src, InetAddress dst, int port, Payload payload) throws Exception { + IpHeader preEspIpHeader = getIpHeader(payload.getProtocolId(), src, dst, payload); + + return new EspHeader( + payload.getProtocolId(), + spi, + 1, // sequence number + CRYPT_KEY, // Same key for auth and crypt + payload.getPacketBytes(preEspIpHeader)); + } + + private EspHeader buildTunnelModeEspPacket( + int spi, + InetAddress srcInner, + InetAddress dstInner, + InetAddress srcOuter, + InetAddress dstOuter, + int port, + int encapPort, + Payload payload) + throws Exception { + IpHeader innerIp = getIpHeader(payload.getProtocolId(), srcInner, dstInner, payload); + return new EspHeader( + innerIp.getProtocolId(), + spi, + 1, // sequence number + CRYPT_KEY, // Same key for auth and crypt + innerIp.getPacketBytes()); + } + + private IpHeader maybeEncapPacket( + InetAddress src, InetAddress dst, int encapPort, EspHeader espPayload) + throws Exception { + + Payload payload = espPayload; + if (encapPort != 0) { + payload = new UdpHeader(encapPort, encapPort, espPayload); + } + + return getIpHeader(payload.getProtocolId(), src, dst, payload); + } + + private byte[] getTunnelModePacket( + int spi, + InetAddress srcInner, + InetAddress dstInner, + InetAddress srcOuter, + InetAddress dstOuter, + int port, + int encapPort) + throws Exception { + UdpHeader udp = new UdpHeader(port, port, new BytePayload(TEST_DATA)); + + EspHeader espPayload = + buildTunnelModeEspPacket( + spi, srcInner, dstInner, srcOuter, dstOuter, port, encapPort, udp); + return maybeEncapPacket(srcOuter, dstOuter, encapPort, espPayload).getPacketBytes(); + } + + private byte[] getTransportInTunnelModePacket( + int spiInner, + int spiOuter, + InetAddress srcInner, + InetAddress dstInner, + InetAddress srcOuter, + InetAddress dstOuter, + int port, + int encapPort) + throws Exception { + UdpHeader udp = new UdpHeader(port, port, new BytePayload(TEST_DATA)); + + EspHeader espPayload = buildTransportModeEspPacket(spiInner, srcInner, dstInner, port, udp); + espPayload = + buildTunnelModeEspPacket( + spiOuter, + srcInner, + dstInner, + srcOuter, + dstOuter, + port, + encapPort, + espPayload); + return maybeEncapPacket(srcOuter, dstOuter, encapPort, espPayload).getPacketBytes(); + } + + // Transport-in-Tunnel mode tests + @Test + public void testTransportInTunnelModeV4InV4() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelOutput(AF_INET, AF_INET, false, true); + checkTunnelInput(AF_INET, AF_INET, false, true); + } + + @Test + public void testTransportInTunnelModeV4InV4Reflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelReflected(AF_INET, AF_INET, false, true); + } + + @Test + public void testTransportInTunnelModeV4InV4UdpEncap() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelOutput(AF_INET, AF_INET, true, true); + checkTunnelInput(AF_INET, AF_INET, true, true); + } + + @Test + public void testTransportInTunnelModeV4InV4UdpEncapReflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelReflected(AF_INET, AF_INET, false, true); + } + + @Test + public void testTransportInTunnelModeV4InV6() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelOutput(AF_INET, AF_INET6, false, true); + checkTunnelInput(AF_INET, AF_INET6, false, true); + } + + @Test + public void testTransportInTunnelModeV4InV6Reflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelReflected(AF_INET, AF_INET, false, true); + } + + @Test + public void testTransportInTunnelModeV6InV4() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelOutput(AF_INET6, AF_INET, false, true); + checkTunnelInput(AF_INET6, AF_INET, false, true); + } + + @Test + public void testTransportInTunnelModeV6InV4Reflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelReflected(AF_INET, AF_INET, false, true); + } + + @Test + public void testTransportInTunnelModeV6InV4UdpEncap() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelOutput(AF_INET6, AF_INET, true, true); + checkTunnelInput(AF_INET6, AF_INET, true, true); + } + + @Test + public void testTransportInTunnelModeV6InV4UdpEncapReflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelReflected(AF_INET, AF_INET, false, true); + } + + @Test + public void testTransportInTunnelModeV6InV6() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelOutput(AF_INET, AF_INET6, false, true); + checkTunnelInput(AF_INET, AF_INET6, false, true); + } + + @Test + public void testTransportInTunnelModeV6InV6Reflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelReflected(AF_INET, AF_INET, false, true); + } + + // Tunnel mode tests + @Test + public void testTunnelV4InV4() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelOutput(AF_INET, AF_INET, false, false); + checkTunnelInput(AF_INET, AF_INET, false, false); + } + + @Test + public void testTunnelV4InV4Reflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelReflected(AF_INET, AF_INET, false, false); + } + + @Test + public void testTunnelV4InV4UdpEncap() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelOutput(AF_INET, AF_INET, true, false); + checkTunnelInput(AF_INET, AF_INET, true, false); + } + + @Test + public void testTunnelV4InV4UdpEncapReflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelReflected(AF_INET, AF_INET, true, false); + } + + @Test + public void testTunnelV4InV6() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelOutput(AF_INET, AF_INET6, false, false); + checkTunnelInput(AF_INET, AF_INET6, false, false); + } + + @Test + public void testTunnelV4InV6Reflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelReflected(AF_INET, AF_INET6, false, false); + } + + @Test + public void testTunnelV6InV4() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelOutput(AF_INET6, AF_INET, false, false); + checkTunnelInput(AF_INET6, AF_INET, false, false); + } + + @Test + public void testTunnelV6InV4Reflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelReflected(AF_INET6, AF_INET, false, false); + } + + @Test + public void testTunnelV6InV4UdpEncap() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelOutput(AF_INET6, AF_INET, true, false); + checkTunnelInput(AF_INET6, AF_INET, true, false); + } + + @Test + public void testTunnelV6InV4UdpEncapReflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelReflected(AF_INET6, AF_INET, true, false); + } + + @Test + public void testTunnelV6InV6() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelOutput(AF_INET6, AF_INET6, false, false); + checkTunnelInput(AF_INET6, AF_INET6, false, false); + } + + @Test + public void testTunnelV6InV6Reflected() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + checkTunnelReflected(AF_INET6, AF_INET6, false, false); + } +} diff --git a/tests/cts/net/src/android/net/cts/LocalServerSocketTest.java b/tests/cts/net/src/android/net/cts/LocalServerSocketTest.java new file mode 100644 index 0000000000..7c5a1b353d --- /dev/null +++ b/tests/cts/net/src/android/net/cts/LocalServerSocketTest.java @@ -0,0 +1,61 @@ +/* + * 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.cts; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import android.net.LocalServerSocket; +import android.net.LocalSocket; +import android.net.LocalSocketAddress; + +public class LocalServerSocketTest extends TestCase { + + public void testLocalServerSocket() throws IOException { + String address = "com.android.net.LocalServerSocketTest_testLocalServerSocket"; + LocalServerSocket localServerSocket = new LocalServerSocket(address); + assertNotNull(localServerSocket.getLocalSocketAddress()); + + // create client socket + LocalSocket clientSocket = new LocalSocket(); + + // establish connection between client and server + clientSocket.connect(new LocalSocketAddress(address)); + LocalSocket serverSocket = localServerSocket.accept(); + + assertTrue(serverSocket.isConnected()); + assertTrue(serverSocket.isBound()); + + // send data from client to server + OutputStream clientOutStream = clientSocket.getOutputStream(); + clientOutStream.write(12); + InputStream serverInStream = serverSocket.getInputStream(); + assertEquals(12, serverInStream.read()); + + // send data from server to client + OutputStream serverOutStream = serverSocket.getOutputStream(); + serverOutStream.write(3); + InputStream clientInStream = clientSocket.getInputStream(); + assertEquals(3, clientInStream.read()); + + // close server socket + assertNotNull(localServerSocket.getFileDescriptor()); + localServerSocket.close(); + assertNull(localServerSocket.getFileDescriptor()); + } +} diff --git a/tests/cts/net/src/android/net/cts/LocalSocketAddressTest.java b/tests/cts/net/src/android/net/cts/LocalSocketAddressTest.java new file mode 100644 index 0000000000..6ef003b26f --- /dev/null +++ b/tests/cts/net/src/android/net/cts/LocalSocketAddressTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2008 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.cts; + +import android.net.LocalSocketAddress; +import android.net.LocalSocketAddress.Namespace; +import android.test.AndroidTestCase; + +public class LocalSocketAddressTest extends AndroidTestCase { + + public void testNewLocalSocketAddressWithDefaultNamespace() { + // default namespace + LocalSocketAddress localSocketAddress = new LocalSocketAddress("name"); + assertEquals("name", localSocketAddress.getName()); + assertEquals(Namespace.ABSTRACT, localSocketAddress.getNamespace()); + + // specify the namespace + LocalSocketAddress localSocketAddress2 = + new LocalSocketAddress("name2", Namespace.ABSTRACT); + assertEquals("name2", localSocketAddress2.getName()); + assertEquals(Namespace.ABSTRACT, localSocketAddress2.getNamespace()); + + LocalSocketAddress localSocketAddress3 = + new LocalSocketAddress("name3", Namespace.FILESYSTEM); + assertEquals("name3", localSocketAddress3.getName()); + assertEquals(Namespace.FILESYSTEM, localSocketAddress3.getNamespace()); + + LocalSocketAddress localSocketAddress4 = + new LocalSocketAddress("name4", Namespace.RESERVED); + assertEquals("name4", localSocketAddress4.getName()); + assertEquals(Namespace.RESERVED, localSocketAddress4.getNamespace()); + } +} diff --git a/tests/cts/net/src/android/net/cts/LocalSocketAddress_NamespaceTest.java b/tests/cts/net/src/android/net/cts/LocalSocketAddress_NamespaceTest.java new file mode 100644 index 0000000000..97dfa435fa --- /dev/null +++ b/tests/cts/net/src/android/net/cts/LocalSocketAddress_NamespaceTest.java @@ -0,0 +1,36 @@ +/* + * 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.cts; + +import android.net.LocalSocketAddress.Namespace; +import android.test.AndroidTestCase; + +public class LocalSocketAddress_NamespaceTest extends AndroidTestCase { + + public void testValueOf() { + assertEquals(Namespace.ABSTRACT, Namespace.valueOf("ABSTRACT")); + assertEquals(Namespace.RESERVED, Namespace.valueOf("RESERVED")); + assertEquals(Namespace.FILESYSTEM, Namespace.valueOf("FILESYSTEM")); + } + + public void testValues() { + Namespace[] expected = Namespace.values(); + assertEquals(Namespace.ABSTRACT, expected[0]); + assertEquals(Namespace.RESERVED, expected[1]); + assertEquals(Namespace.FILESYSTEM, expected[2]); + } +} diff --git a/tests/cts/net/src/android/net/cts/LocalSocketTest.java b/tests/cts/net/src/android/net/cts/LocalSocketTest.java new file mode 100644 index 0000000000..6e61705b92 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/LocalSocketTest.java @@ -0,0 +1,470 @@ +/* + * Copyright (C) 2008 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.cts; + +import junit.framework.TestCase; + +import android.net.Credentials; +import android.net.LocalServerSocket; +import android.net.LocalSocket; +import android.net.LocalSocketAddress; +import android.system.Os; +import android.system.OsConstants; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +public class LocalSocketTest extends TestCase { + private final static String ADDRESS_PREFIX = "com.android.net.LocalSocketTest"; + + public void testLocalConnections() throws IOException { + String address = ADDRESS_PREFIX + "_testLocalConnections"; + // create client and server socket + LocalServerSocket localServerSocket = new LocalServerSocket(address); + LocalSocket clientSocket = new LocalSocket(); + + // establish connection between client and server + LocalSocketAddress locSockAddr = new LocalSocketAddress(address); + assertFalse(clientSocket.isConnected()); + clientSocket.connect(locSockAddr); + assertTrue(clientSocket.isConnected()); + + LocalSocket serverSocket = localServerSocket.accept(); + assertTrue(serverSocket.isConnected()); + assertTrue(serverSocket.isBound()); + try { + serverSocket.bind(localServerSocket.getLocalSocketAddress()); + fail("Cannot bind a LocalSocket from accept()"); + } catch (IOException expected) { + } + try { + serverSocket.connect(locSockAddr); + fail("Cannot connect a LocalSocket from accept()"); + } catch (IOException expected) { + } + + Credentials credent = clientSocket.getPeerCredentials(); + assertTrue(0 != credent.getPid()); + + // send data from client to server + OutputStream clientOutStream = clientSocket.getOutputStream(); + clientOutStream.write(12); + InputStream serverInStream = serverSocket.getInputStream(); + assertEquals(12, serverInStream.read()); + + //send data from server to client + OutputStream serverOutStream = serverSocket.getOutputStream(); + serverOutStream.write(3); + InputStream clientInStream = clientSocket.getInputStream(); + assertEquals(3, clientInStream.read()); + + // Test sending and receiving file descriptors + clientSocket.setFileDescriptorsForSend(new FileDescriptor[]{FileDescriptor.in}); + clientOutStream.write(32); + assertEquals(32, serverInStream.read()); + + FileDescriptor[] out = serverSocket.getAncillaryFileDescriptors(); + assertEquals(1, out.length); + FileDescriptor fd = clientSocket.getFileDescriptor(); + assertTrue(fd.valid()); + + //shutdown input stream of client + clientSocket.shutdownInput(); + assertEquals(-1, clientInStream.read()); + + //shutdown output stream of client + clientSocket.shutdownOutput(); + try { + clientOutStream.write(10); + fail("testLocalSocket shouldn't come to here"); + } catch (IOException e) { + // expected + } + + //shutdown input stream of server + serverSocket.shutdownInput(); + assertEquals(-1, serverInStream.read()); + + //shutdown output stream of server + serverSocket.shutdownOutput(); + try { + serverOutStream.write(10); + fail("testLocalSocket shouldn't come to here"); + } catch (IOException e) { + // expected + } + + //close client socket + clientSocket.close(); + try { + clientInStream.read(); + fail("testLocalSocket shouldn't come to here"); + } catch (IOException e) { + // expected + } + + //close server socket + serverSocket.close(); + try { + serverInStream.read(); + fail("testLocalSocket shouldn't come to here"); + } catch (IOException e) { + // expected + } + } + + public void testAccessors() throws IOException { + String address = ADDRESS_PREFIX + "_testAccessors"; + LocalSocket socket = new LocalSocket(); + LocalSocketAddress addr = new LocalSocketAddress(address); + + assertFalse(socket.isBound()); + socket.bind(addr); + assertTrue(socket.isBound()); + assertEquals(addr, socket.getLocalSocketAddress()); + + String str = socket.toString(); + assertTrue(str.contains("impl:android.net.LocalSocketImpl")); + + socket.setReceiveBufferSize(1999); + assertEquals(1999 << 1, socket.getReceiveBufferSize()); + + socket.setSendBufferSize(3998); + assertEquals(3998 << 1, socket.getSendBufferSize()); + + assertEquals(0, socket.getSoTimeout()); + socket.setSoTimeout(1996); + assertTrue(socket.getSoTimeout() > 0); + + try { + socket.getRemoteSocketAddress(); + fail("testLocalSocketSecondary shouldn't come to here"); + } catch (UnsupportedOperationException e) { + // expected + } + + try { + socket.isClosed(); + fail("testLocalSocketSecondary shouldn't come to here"); + } catch (UnsupportedOperationException e) { + // expected + } + + try { + socket.isInputShutdown(); + fail("testLocalSocketSecondary shouldn't come to here"); + } catch (UnsupportedOperationException e) { + // expected + } + + try { + socket.isOutputShutdown(); + fail("testLocalSocketSecondary shouldn't come to here"); + } catch (UnsupportedOperationException e) { + // expected + } + + try { + socket.connect(addr, 2005); + fail("testLocalSocketSecondary shouldn't come to here"); + } catch (UnsupportedOperationException e) { + // expected + } + + socket.close(); + } + + // http://b/31205169 + public void testSetSoTimeout_readTimeout() throws Exception { + String address = ADDRESS_PREFIX + "_testSetSoTimeout_readTimeout"; + + try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) { + final LocalSocket clientSocket = socketPair.clientSocket; + + // Set the timeout in millis. + int timeoutMillis = 1000; + clientSocket.setSoTimeout(timeoutMillis); + + // Avoid blocking the test run if timeout doesn't happen by using a separate thread. + Callable reader = () -> { + try { + clientSocket.getInputStream().read(); + return Result.noException("Did not block"); + } catch (IOException e) { + return Result.exception(e); + } + }; + // Allow the configured timeout, plus some slop. + int allowedTime = timeoutMillis + 2000; + Result result = runInSeparateThread(allowedTime, reader); + + // Check the message was a timeout, it's all we have to go on. + String expectedMessage = Os.strerror(OsConstants.EAGAIN); + result.assertThrewIOException(expectedMessage); + } + } + + // http://b/31205169 + public void testSetSoTimeout_writeTimeout() throws Exception { + String address = ADDRESS_PREFIX + "_testSetSoTimeout_writeTimeout"; + + try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) { + final LocalSocket clientSocket = socketPair.clientSocket; + + // Set the timeout in millis. + int timeoutMillis = 1000; + clientSocket.setSoTimeout(timeoutMillis); + + // Set a small buffer size so we know we can flood it. + clientSocket.setSendBufferSize(100); + final int bufferSize = clientSocket.getSendBufferSize(); + + // Avoid blocking the test run if timeout doesn't happen by using a separate thread. + Callable writer = () -> { + try { + byte[] toWrite = new byte[bufferSize * 2]; + clientSocket.getOutputStream().write(toWrite); + return Result.noException("Did not block"); + } catch (IOException e) { + return Result.exception(e); + } + }; + // Allow the configured timeout, plus some slop. + int allowedTime = timeoutMillis + 2000; + + Result result = runInSeparateThread(allowedTime, writer); + + // Check the message was a timeout, it's all we have to go on. + String expectedMessage = Os.strerror(OsConstants.EAGAIN); + result.assertThrewIOException(expectedMessage); + } + } + + public void testAvailable() throws Exception { + String address = ADDRESS_PREFIX + "_testAvailable"; + + try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) { + LocalSocket clientSocket = socketPair.clientSocket; + LocalSocket serverSocket = socketPair.serverSocket.accept(); + + OutputStream clientOutputStream = clientSocket.getOutputStream(); + InputStream serverInputStream = serverSocket.getInputStream(); + assertEquals(0, serverInputStream.available()); + + byte[] buffer = new byte[50]; + clientOutputStream.write(buffer); + assertEquals(50, serverInputStream.available()); + + InputStream clientInputStream = clientSocket.getInputStream(); + OutputStream serverOutputStream = serverSocket.getOutputStream(); + assertEquals(0, clientInputStream.available()); + serverOutputStream.write(buffer); + assertEquals(50, serverInputStream.available()); + + serverSocket.close(); + } + } + + // http://b/34095140 + public void testLocalSocketCreatedFromFileDescriptor() throws Exception { + String address = ADDRESS_PREFIX + "_testLocalSocketCreatedFromFileDescriptor"; + + // Establish connection between a local client and server to get a valid client socket file + // descriptor. + try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) { + // Extract the client FileDescriptor we can use. + FileDescriptor fileDescriptor = socketPair.clientSocket.getFileDescriptor(); + assertTrue(fileDescriptor.valid()); + + // Create the LocalSocket we want to test. + LocalSocket clientSocketCreatedFromFileDescriptor = + LocalSocket.createConnectedLocalSocket(fileDescriptor); + assertTrue(clientSocketCreatedFromFileDescriptor.isConnected()); + assertTrue(clientSocketCreatedFromFileDescriptor.isBound()); + + // Test the LocalSocket can be used for communication. + LocalSocket serverSocket = socketPair.serverSocket.accept(); + OutputStream clientOutputStream = + clientSocketCreatedFromFileDescriptor.getOutputStream(); + InputStream serverInputStream = serverSocket.getInputStream(); + + clientOutputStream.write(12); + assertEquals(12, serverInputStream.read()); + + // Closing clientSocketCreatedFromFileDescriptor does not close the file descriptor. + clientSocketCreatedFromFileDescriptor.close(); + assertTrue(fileDescriptor.valid()); + + // .. while closing the LocalSocket that owned the file descriptor does. + socketPair.clientSocket.close(); + assertFalse(fileDescriptor.valid()); + } + } + + public void testFlush() throws Exception { + String address = ADDRESS_PREFIX + "_testFlush"; + + try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) { + LocalSocket clientSocket = socketPair.clientSocket; + LocalSocket serverSocket = socketPair.serverSocket.accept(); + + OutputStream clientOutputStream = clientSocket.getOutputStream(); + InputStream serverInputStream = serverSocket.getInputStream(); + testFlushWorks(clientOutputStream, serverInputStream); + + OutputStream serverOutputStream = serverSocket.getOutputStream(); + InputStream clientInputStream = clientSocket.getInputStream(); + testFlushWorks(serverOutputStream, clientInputStream); + + serverSocket.close(); + } + } + + private void testFlushWorks(OutputStream outputStream, InputStream inputStream) + throws Exception { + final int bytesToTransfer = 50; + StreamReader inputStreamReader = new StreamReader(inputStream, bytesToTransfer); + + byte[] buffer = new byte[bytesToTransfer]; + outputStream.write(buffer); + assertEquals(bytesToTransfer, inputStream.available()); + + // Start consuming the data. + inputStreamReader.start(); + + // This doesn't actually flush any buffers, it just polls until the reader has read all the + // bytes. + outputStream.flush(); + + inputStreamReader.waitForCompletion(5000); + inputStreamReader.assertBytesRead(bytesToTransfer); + assertEquals(0, inputStream.available()); + } + + private static class StreamReader extends Thread { + private final InputStream is; + private final int expectedByteCount; + private final CountDownLatch completeLatch = new CountDownLatch(1); + + private volatile Exception exception; + private int bytesRead; + + private StreamReader(InputStream is, int expectedByteCount) { + this.is = is; + this.expectedByteCount = expectedByteCount; + } + + @Override + public void run() { + try { + byte[] buffer = new byte[10]; + int readCount; + while ((readCount = is.read(buffer)) >= 0) { + bytesRead += readCount; + if (bytesRead >= expectedByteCount) { + break; + } + } + } catch (IOException e) { + exception = e; + } finally { + completeLatch.countDown(); + } + } + + public void waitForCompletion(long waitMillis) throws Exception { + if (!completeLatch.await(waitMillis, TimeUnit.MILLISECONDS)) { + fail("Timeout waiting for completion"); + } + if (exception != null) { + throw new Exception("Read failed", exception); + } + } + + public void assertBytesRead(int expected) { + assertEquals(expected, bytesRead); + } + } + + private static class Result { + private final String type; + private final Exception e; + + private Result(String type, Exception e) { + this.type = type; + this.e = e; + } + + static Result noException(String description) { + return new Result(description, null); + } + + static Result exception(Exception e) { + return new Result(e.getClass().getName(), e); + } + + void assertThrewIOException(String expectedMessage) { + assertEquals("Unexpected result type", IOException.class.getName(), type); + assertEquals("Unexpected exception message", expectedMessage, e.getMessage()); + } + } + + private static Result runInSeparateThread(int allowedTime, final Callable callable) + throws Exception { + ExecutorService service = Executors.newSingleThreadScheduledExecutor(); + Future future = service.submit(callable); + Result result = future.get(allowedTime, TimeUnit.MILLISECONDS); + if (!future.isDone()) { + fail("Worker thread appears blocked"); + } + return result; + } + + private static class LocalSocketPair implements AutoCloseable { + static LocalSocketPair createConnectedSocketPair(String address) throws Exception { + LocalServerSocket localServerSocket = new LocalServerSocket(address); + final LocalSocket clientSocket = new LocalSocket(); + + // Establish connection between client and server + LocalSocketAddress locSockAddr = new LocalSocketAddress(address); + clientSocket.connect(locSockAddr); + assertTrue(clientSocket.isConnected()); + return new LocalSocketPair(localServerSocket, clientSocket); + } + + final LocalServerSocket serverSocket; + final LocalSocket clientSocket; + + LocalSocketPair(LocalServerSocket serverSocket, LocalSocket clientSocket) { + this.serverSocket = serverSocket; + this.clientSocket = clientSocket; + } + + public void close() throws Exception { + serverSocket.close(); + clientSocket.close(); + } + } +} diff --git a/tests/cts/net/src/android/net/cts/MacAddressTest.java b/tests/cts/net/src/android/net/cts/MacAddressTest.java new file mode 100644 index 0000000000..3fd3bbac8c --- /dev/null +++ b/tests/cts/net/src/android/net/cts/MacAddressTest.java @@ -0,0 +1,223 @@ +/* + * 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.cts; + +import static android.net.MacAddress.TYPE_BROADCAST; +import static android.net.MacAddress.TYPE_MULTICAST; +import static android.net.MacAddress.TYPE_UNICAST; + +import static com.android.testutils.ParcelUtils.assertParcelSane; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.net.MacAddress; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.Inet6Address; +import java.util.Arrays; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class MacAddressTest { + + static class TestCase { + final String macAddress; + final String ouiString; + final int addressType; + final boolean isLocallyAssigned; + + TestCase(String macAddress, String ouiString, int addressType, boolean isLocallyAssigned) { + this.macAddress = macAddress; + this.ouiString = ouiString; + this.addressType = addressType; + this.isLocallyAssigned = isLocallyAssigned; + } + } + + static final boolean LOCALLY_ASSIGNED = true; + static final boolean GLOBALLY_UNIQUE = false; + + static String typeToString(int addressType) { + switch (addressType) { + case TYPE_UNICAST: + return "TYPE_UNICAST"; + case TYPE_BROADCAST: + return "TYPE_BROADCAST"; + case TYPE_MULTICAST: + return "TYPE_MULTICAST"; + default: + return "UNKNOWN"; + } + } + + static String localAssignedToString(boolean isLocallyAssigned) { + return isLocallyAssigned ? "LOCALLY_ASSIGNED" : "GLOBALLY_UNIQUE"; + } + + @Test + public void testMacAddress() { + TestCase[] tests = { + new TestCase("ff:ff:ff:ff:ff:ff", "ff:ff:ff", TYPE_BROADCAST, LOCALLY_ASSIGNED), + new TestCase("d2:c4:22:4d:32:a8", "d2:c4:22", TYPE_UNICAST, LOCALLY_ASSIGNED), + new TestCase("33:33:aa:bb:cc:dd", "33:33:aa", TYPE_MULTICAST, LOCALLY_ASSIGNED), + new TestCase("06:00:00:00:00:00", "06:00:00", TYPE_UNICAST, LOCALLY_ASSIGNED), + new TestCase("07:00:d3:56:8a:c4", "07:00:d3", TYPE_MULTICAST, LOCALLY_ASSIGNED), + new TestCase("00:01:44:55:66:77", "00:01:44", TYPE_UNICAST, GLOBALLY_UNIQUE), + new TestCase("08:00:22:33:44:55", "08:00:22", TYPE_UNICAST, GLOBALLY_UNIQUE), + }; + + for (TestCase tc : tests) { + MacAddress mac = MacAddress.fromString(tc.macAddress); + + if (!tc.ouiString.equals(mac.toOuiString())) { + fail(String.format("expected OUI string %s, got %s", + tc.ouiString, mac.toOuiString())); + } + + if (tc.isLocallyAssigned != mac.isLocallyAssigned()) { + fail(String.format("expected %s to be %s, got %s", mac, + localAssignedToString(tc.isLocallyAssigned), + localAssignedToString(mac.isLocallyAssigned()))); + } + + if (tc.addressType != mac.getAddressType()) { + fail(String.format("expected %s address type to be %s, got %s", mac, + typeToString(tc.addressType), typeToString(mac.getAddressType()))); + } + + if (!tc.macAddress.equals(mac.toString())) { + fail(String.format("expected toString() to return %s, got %s", + tc.macAddress, mac.toString())); + } + + if (!mac.equals(MacAddress.fromBytes(mac.toByteArray()))) { + byte[] bytes = mac.toByteArray(); + fail(String.format("expected mac address from bytes %s to be %s, got %s", + Arrays.toString(bytes), + MacAddress.fromBytes(bytes), + mac)); + } + } + } + + @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() { + final MacAddress mac = MacAddress.fromString("52:74:f2:b1:a8:7f"); + final 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}; + final Inet6Address llv6 = mac.getLinkLocalIpv6FromEui48Mac(); + assertTrue(llv6.isLinkLocalAddress()); + assertArrayEquals(inet6ll, llv6.getAddress()); + } + + @Test + public void testParcelMacAddress() { + final MacAddress mac = MacAddress.fromString("52:74:f2:b1:a8:7f"); + + assertParcelSane(mac, 1); + } +} diff --git a/tests/cts/net/src/android/net/cts/MailToTest.java b/tests/cts/net/src/android/net/cts/MailToTest.java new file mode 100644 index 0000000000..e454d20628 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/MailToTest.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2008 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.cts; + +import android.net.MailTo; +import android.test.AndroidTestCase; +import android.util.Log; + +public class MailToTest extends AndroidTestCase { + private static final String MAILTOURI_1 = "mailto:chris@example.com"; + private static final String MAILTOURI_2 = "mailto:infobot@example.com?subject=current-issue"; + private static final String MAILTOURI_3 = + "mailto:infobot@example.com?body=send%20current-issue"; + private static final String MAILTOURI_4 = "mailto:infobot@example.com?body=send%20current-" + + "issue%0D%0Asend%20index"; + private static final String MAILTOURI_5 = "mailto:joe@example.com?" + + "cc=bob@example.com&body=hello"; + private static final String MAILTOURI_6 = "mailto:?to=joe@example.com&" + + "cc=bob@example.com&body=hello"; + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + public void testParseMailToURI() { + assertFalse(MailTo.isMailTo(null)); + assertFalse(MailTo.isMailTo("")); + assertFalse(MailTo.isMailTo("http://www.google.com")); + + assertTrue(MailTo.isMailTo(MAILTOURI_1)); + MailTo mailTo_1 = MailTo.parse(MAILTOURI_1); + Log.d("Trace", mailTo_1.toString()); + assertEquals("chris@example.com", mailTo_1.getTo()); + assertEquals(1, mailTo_1.getHeaders().size()); + assertNull(mailTo_1.getBody()); + assertNull(mailTo_1.getCc()); + assertNull(mailTo_1.getSubject()); + assertEquals("mailto:?to=chris%40example.com&", mailTo_1.toString()); + + assertTrue(MailTo.isMailTo(MAILTOURI_2)); + MailTo mailTo_2 = MailTo.parse(MAILTOURI_2); + Log.d("Trace", mailTo_2.toString()); + assertEquals(2, mailTo_2.getHeaders().size()); + assertEquals("infobot@example.com", mailTo_2.getTo()); + assertEquals("current-issue", mailTo_2.getSubject()); + assertNull(mailTo_2.getBody()); + assertNull(mailTo_2.getCc()); + String stringUrl = mailTo_2.toString(); + assertTrue(stringUrl.startsWith("mailto:?")); + assertTrue(stringUrl.contains("to=infobot%40example.com&")); + assertTrue(stringUrl.contains("subject=current-issue&")); + + assertTrue(MailTo.isMailTo(MAILTOURI_3)); + MailTo mailTo_3 = MailTo.parse(MAILTOURI_3); + Log.d("Trace", mailTo_3.toString()); + assertEquals(2, mailTo_3.getHeaders().size()); + assertEquals("infobot@example.com", mailTo_3.getTo()); + assertEquals("send current-issue", mailTo_3.getBody()); + assertNull(mailTo_3.getCc()); + assertNull(mailTo_3.getSubject()); + stringUrl = mailTo_3.toString(); + assertTrue(stringUrl.startsWith("mailto:?")); + assertTrue(stringUrl.contains("to=infobot%40example.com&")); + assertTrue(stringUrl.contains("body=send%20current-issue&")); + + assertTrue(MailTo.isMailTo(MAILTOURI_4)); + MailTo mailTo_4 = MailTo.parse(MAILTOURI_4); + Log.d("Trace", mailTo_4.toString() + " " + mailTo_4.getBody()); + assertEquals(2, mailTo_4.getHeaders().size()); + assertEquals("infobot@example.com", mailTo_4.getTo()); + assertEquals("send current-issue\r\nsend index", mailTo_4.getBody()); + assertNull(mailTo_4.getCc()); + assertNull(mailTo_4.getSubject()); + stringUrl = mailTo_4.toString(); + assertTrue(stringUrl.startsWith("mailto:?")); + assertTrue(stringUrl.contains("to=infobot%40example.com&")); + assertTrue(stringUrl.contains("body=send%20current-issue%0D%0Asend%20index&")); + + + assertTrue(MailTo.isMailTo(MAILTOURI_5)); + MailTo mailTo_5 = MailTo.parse(MAILTOURI_5); + Log.d("Trace", mailTo_5.toString() + mailTo_5.getHeaders().toString() + + mailTo_5.getHeaders().size()); + assertEquals(3, mailTo_5.getHeaders().size()); + assertEquals("joe@example.com", mailTo_5.getTo()); + assertEquals("bob@example.com", mailTo_5.getCc()); + assertEquals("hello", mailTo_5.getBody()); + assertNull(mailTo_5.getSubject()); + stringUrl = mailTo_5.toString(); + assertTrue(stringUrl.startsWith("mailto:?")); + assertTrue(stringUrl.contains("cc=bob%40example.com&")); + assertTrue(stringUrl.contains("body=hello&")); + assertTrue(stringUrl.contains("to=joe%40example.com&")); + + assertTrue(MailTo.isMailTo(MAILTOURI_6)); + MailTo mailTo_6 = MailTo.parse(MAILTOURI_6); + Log.d("Trace", mailTo_6.toString() + mailTo_6.getHeaders().toString() + + mailTo_6.getHeaders().size()); + assertEquals(3, mailTo_6.getHeaders().size()); + assertEquals(", joe@example.com", mailTo_6.getTo()); + assertEquals("bob@example.com", mailTo_6.getCc()); + assertEquals("hello", mailTo_6.getBody()); + assertNull(mailTo_6.getSubject()); + stringUrl = mailTo_6.toString(); + assertTrue(stringUrl.startsWith("mailto:?")); + assertTrue(stringUrl.contains("cc=bob%40example.com&")); + assertTrue(stringUrl.contains("body=hello&")); + assertTrue(stringUrl.contains("to=%2C%20joe%40example.com&")); + } +} diff --git a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java new file mode 100644 index 0000000000..691ab99235 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java @@ -0,0 +1,242 @@ +/* + * 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.cts; + +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; + +import android.content.Context; +import android.content.ContentResolver; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkUtils; +import android.net.cts.util.CtsNetUtils; +import android.platform.test.annotations.AppModeFull; +import android.provider.Settings; +import android.system.ErrnoException; +import android.system.OsConstants; +import android.test.AndroidTestCase; + +import java.util.ArrayList; + +public class MultinetworkApiTest extends AndroidTestCase { + + static { + System.loadLibrary("nativemultinetwork_jni"); + } + + private static final String TAG = "MultinetworkNativeApiTest"; + static final String GOOGLE_PRIVATE_DNS_SERVER = "dns.google"; + + /** + * @return 0 on success + */ + private static native int runGetaddrinfoCheck(long networkHandle); + private static native int runSetprocnetwork(long networkHandle); + private static native int runSetsocknetwork(long networkHandle); + private static native int runDatagramCheck(long networkHandle); + private static native void runResNapiMalformedCheck(long networkHandle); + private static native void runResNcancelCheck(long networkHandle); + private static native void runResNqueryCheck(long networkHandle); + private static native void runResNsendCheck(long networkHandle); + private static native void runResNnxDomainCheck(long networkHandle); + + + private ContentResolver mCR; + private ConnectivityManager mCM; + private CtsNetUtils mCtsNetUtils; + private String mOldMode; + private String mOldDnsSpecifier; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mCM = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE); + mCR = getContext().getContentResolver(); + mCtsNetUtils = new CtsNetUtils(getContext()); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + private Network[] getTestableNetworks() { + final ArrayList testableNetworks = new ArrayList(); + for (Network network : mCM.getAllNetworks()) { + final NetworkCapabilities nc = mCM.getNetworkCapabilities(network); + if (nc != null + && nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) + && nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { + testableNetworks.add(network); + } + } + + assertTrue( + "This test requires that at least one network be connected. " + + "Please ensure that the device is connected to a network.", + testableNetworks.size() >= 1); + return testableNetworks.toArray(new Network[0]); + } + + public void testGetaddrinfo() throws ErrnoException { + for (Network network : getTestableNetworks()) { + int errno = runGetaddrinfoCheck(network.getNetworkHandle()); + if (errno != 0) { + throw new ErrnoException( + "getaddrinfo on " + mCM.getNetworkInfo(network), -errno); + } + } + } + + @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps") + public void testSetprocnetwork() throws ErrnoException { + // Hopefully no prior test in this process space has set a default network. + assertNull(mCM.getProcessDefaultNetwork()); + assertEquals(0, NetworkUtils.getBoundNetworkForProcess()); + + for (Network network : getTestableNetworks()) { + mCM.setProcessDefaultNetwork(null); + assertNull(mCM.getProcessDefaultNetwork()); + + int errno = runSetprocnetwork(network.getNetworkHandle()); + if (errno != 0) { + throw new ErrnoException( + "setprocnetwork on " + mCM.getNetworkInfo(network), -errno); + } + Network processDefault = mCM.getProcessDefaultNetwork(); + assertNotNull(processDefault); + assertEquals(network, processDefault); + // TODO: open DatagramSockets, connect them to 192.0.2.1 and 2001:db8::, + // and ensure that the source address is in fact on this network as + // determined by mCM.getLinkProperties(network). + + mCM.setProcessDefaultNetwork(null); + } + + for (Network network : getTestableNetworks()) { + NetworkUtils.bindProcessToNetwork(0); + assertNull(mCM.getBoundNetworkForProcess()); + + int errno = runSetprocnetwork(network.getNetworkHandle()); + if (errno != 0) { + throw new ErrnoException( + "setprocnetwork on " + mCM.getNetworkInfo(network), -errno); + } + assertEquals(network, new Network(mCM.getBoundNetworkForProcess())); + // TODO: open DatagramSockets, connect them to 192.0.2.1 and 2001:db8::, + // and ensure that the source address is in fact on this network as + // determined by mCM.getLinkProperties(network). + + NetworkUtils.bindProcessToNetwork(0); + } + } + + @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps") + public void testSetsocknetwork() throws ErrnoException { + for (Network network : getTestableNetworks()) { + int errno = runSetsocknetwork(network.getNetworkHandle()); + if (errno != 0) { + throw new ErrnoException( + "setsocknetwork on " + mCM.getNetworkInfo(network), -errno); + } + } + } + + public void testNativeDatagramTransmission() throws ErrnoException { + for (Network network : getTestableNetworks()) { + int errno = runDatagramCheck(network.getNetworkHandle()); + if (errno != 0) { + throw new ErrnoException( + "DatagramCheck on " + mCM.getNetworkInfo(network), -errno); + } + } + } + + public void testNoSuchNetwork() { + final Network eNoNet = new Network(54321); + assertNull(mCM.getNetworkInfo(eNoNet)); + + final long eNoNetHandle = eNoNet.getNetworkHandle(); + assertEquals(-OsConstants.ENONET, runSetsocknetwork(eNoNetHandle)); + assertEquals(-OsConstants.ENONET, runSetprocnetwork(eNoNetHandle)); + // TODO: correct test permissions so this call is not silently re-mapped + // to query on the default network. + // assertEquals(-OsConstants.ENONET, runGetaddrinfoCheck(eNoNetHandle)); + } + + public void testNetworkHandle() { + // Test Network -> NetworkHandle -> Network results in the same Network. + for (Network network : getTestableNetworks()) { + long networkHandle = network.getNetworkHandle(); + Network newNetwork = Network.fromNetworkHandle(networkHandle); + assertEquals(newNetwork, network); + } + + // Test that only obfuscated handles are allowed. + try { + Network.fromNetworkHandle(100); + fail(); + } catch (IllegalArgumentException e) {} + try { + Network.fromNetworkHandle(-1); + fail(); + } catch (IllegalArgumentException e) {} + try { + Network.fromNetworkHandle(0); + fail(); + } catch (IllegalArgumentException e) {} + } + + public void testResNApi() throws Exception { + final Network[] testNetworks = getTestableNetworks(); + + for (Network network : testNetworks) { + // Throws AssertionError directly in jni function if test fail. + runResNqueryCheck(network.getNetworkHandle()); + runResNsendCheck(network.getNetworkHandle()); + runResNcancelCheck(network.getNetworkHandle()); + runResNapiMalformedCheck(network.getNetworkHandle()); + + final NetworkCapabilities nc = mCM.getNetworkCapabilities(network); + // Some cellular networks configure their DNS servers never to return NXDOMAIN, so don't + // test NXDOMAIN on these DNS servers. + // b/144521720 + if (nc != null && !nc.hasTransport(TRANSPORT_CELLULAR)) { + runResNnxDomainCheck(network.getNetworkHandle()); + } + } + } + + @AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps") + public void testResNApiNXDomainPrivateDns() throws InterruptedException { + mCtsNetUtils.storePrivateDnsSetting(); + // Enable private DNS strict mode and set server to dns.google before doing NxDomain test. + // b/144521720 + try { + mCtsNetUtils.setPrivateDnsStrictMode(GOOGLE_PRIVATE_DNS_SERVER); + for (Network network : getTestableNetworks()) { + // Wait for private DNS setting to propagate. + mCtsNetUtils.awaitPrivateDnsSetting("NxDomain test wait private DNS setting timeout", + network, GOOGLE_PRIVATE_DNS_SERVER, true); + runResNnxDomainCheck(network.getNetworkHandle()); + } + } finally { + mCtsNetUtils.restorePrivateDnsSetting(); + } + } +} diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt new file mode 100644 index 0000000000..7508228734 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt @@ -0,0 +1,668 @@ +/* + * 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.cts + +import android.app.Instrumentation +import android.content.Context +import android.net.ConnectivityManager +import android.net.KeepalivePacketData +import android.net.LinkAddress +import android.net.LinkProperties +import android.net.Network +import android.net.NetworkAgent +import android.net.NetworkAgent.CMD_ADD_KEEPALIVE_PACKET_FILTER +import android.net.NetworkAgent.CMD_PREVENT_AUTOMATIC_RECONNECT +import android.net.NetworkAgent.CMD_REMOVE_KEEPALIVE_PACKET_FILTER +import android.net.NetworkAgent.CMD_REPORT_NETWORK_STATUS +import android.net.NetworkAgent.CMD_SAVE_ACCEPT_UNVALIDATED +import android.net.NetworkAgent.CMD_START_SOCKET_KEEPALIVE +import android.net.NetworkAgent.CMD_STOP_SOCKET_KEEPALIVE +import android.net.NetworkAgent.INVALID_NETWORK +import android.net.NetworkAgent.VALID_NETWORK +import android.net.NetworkAgentConfig +import android.net.NetworkCapabilities +import android.net.NetworkInfo +import android.net.NetworkProvider +import android.net.NetworkRequest +import android.net.SocketKeepalive +import android.net.StringNetworkSpecifier +import android.net.Uri +import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnAddKeepalivePacketFilter +import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnAutomaticReconnectDisabled +import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnBandwidthUpdateRequested +import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnNetworkUnwanted +import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnRemoveKeepalivePacketFilter +import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnSaveAcceptUnvalidated +import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnSignalStrengthThresholdsUpdated +import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnStartSocketKeepalive +import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnStopSocketKeepalive +import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnValidationStatus +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper +import android.os.Message +import android.os.Messenger +import androidx.test.InstrumentationRegistry +import androidx.test.runner.AndroidJUnit4 +import com.android.internal.util.AsyncChannel +import com.android.net.module.util.ArrayTrackRecord +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.RecorderCallback.CallbackEntry.Available +import com.android.testutils.RecorderCallback.CallbackEntry.Lost +import com.android.testutils.TestableNetworkCallback +import org.junit.After +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.argThat +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import java.net.InetAddress +import java.time.Duration +import java.util.UUID +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +// This test doesn't really have a constraint on how fast the methods should return. If it's +// going to fail, it will simply wait forever, so setting a high timeout lowers the flake ratio +// without affecting the run time of successful runs. Thus, set a very high timeout. +private const val DEFAULT_TIMEOUT_MS = 5000L +// When waiting for a NetworkCallback to determine there was no timeout, waiting is the +// only possible thing (the relevant handler is the one in the real ConnectivityService, +// and then there is the Binder call), so have a short timeout for this as it will be +// exhausted every time. +private const val NO_CALLBACK_TIMEOUT = 200L +// Any legal score (0~99) for the test network would do, as it is going to be kept up by the +// requests filed by the test and should never match normal internet requests. 70 is the default +// score of Ethernet networks, it's as good a value as any other. +private const val TEST_NETWORK_SCORE = 70 +private const val BETTER_NETWORK_SCORE = 75 +private const val FAKE_NET_ID = 1098 +private val instrumentation: Instrumentation + get() = InstrumentationRegistry.getInstrumentation() +private val realContext: Context + get() = InstrumentationRegistry.getContext() +private fun Message(what: Int, arg1: Int, arg2: Int, obj: Any?) = Message.obtain().also { + it.what = what + it.arg1 = arg1 + it.arg2 = arg2 + it.obj = obj +} + +@RunWith(AndroidJUnit4::class) +class NetworkAgentTest { + @Rule @JvmField + val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q) + + private val LOCAL_IPV4_ADDRESS = InetAddress.parseNumericAddress("192.0.2.1") + private val REMOTE_IPV4_ADDRESS = InetAddress.parseNumericAddress("192.0.2.2") + + private val mCM = realContext.getSystemService(ConnectivityManager::class.java) + private val mHandlerThread = HandlerThread("${javaClass.simpleName} handler thread") + private val mFakeConnectivityService by lazy { FakeConnectivityService(mHandlerThread.looper) } + + private class Provider(context: Context, looper: Looper) : + NetworkProvider(context, looper, "NetworkAgentTest NetworkProvider") + + private val agentsToCleanUp = mutableListOf() + private val callbacksToCleanUp = mutableListOf() + + @Before + fun setUp() { + instrumentation.getUiAutomation().adoptShellPermissionIdentity() + mHandlerThread.start() + } + + @After + fun tearDown() { + agentsToCleanUp.forEach { it.unregister() } + callbacksToCleanUp.forEach { mCM.unregisterNetworkCallback(it) } + mHandlerThread.quitSafely() + instrumentation.getUiAutomation().dropShellPermissionIdentity() + } + + /** + * A fake that helps simulating ConnectivityService talking to a harnessed agent. + * This fake only supports speaking to one harnessed agent at a time because it + * only keeps track of one async channel. + */ + private class FakeConnectivityService(looper: Looper) { + private val CMD_EXPECT_DISCONNECT = 1 + private var disconnectExpected = false + private val msgHistory = ArrayTrackRecord().newReadHead() + private val asyncChannel = AsyncChannel() + private val handler = object : Handler(looper) { + override fun handleMessage(msg: Message) { + msgHistory.add(Message.obtain(msg)) // make a copy as the original will be recycled + when (msg.what) { + CMD_EXPECT_DISCONNECT -> disconnectExpected = true + AsyncChannel.CMD_CHANNEL_HALF_CONNECTED -> + asyncChannel.sendMessage(AsyncChannel.CMD_CHANNEL_FULL_CONNECTION) + AsyncChannel.CMD_CHANNEL_DISCONNECTED -> + if (!disconnectExpected) { + fail("Agent unexpectedly disconnected") + } else { + disconnectExpected = false + } + } + } + } + + fun connect(agentMsngr: Messenger) = asyncChannel.connect(realContext, handler, agentMsngr) + + fun disconnect() = asyncChannel.disconnect() + + fun sendMessage(what: Int, arg1: Int = 0, arg2: Int = 0, obj: Any? = null) = + asyncChannel.sendMessage(Message(what, arg1, arg2, obj)) + + fun expectMessage(what: Int) = + assertNotNull(msgHistory.poll(DEFAULT_TIMEOUT_MS) { it.what == what }) + + fun willExpectDisconnectOnce() = handler.sendEmptyMessage(CMD_EXPECT_DISCONNECT) + } + + private open class TestableNetworkAgent( + context: Context, + looper: Looper, + val nc: NetworkCapabilities, + val lp: LinkProperties, + conf: NetworkAgentConfig + ) : NetworkAgent(context, looper, TestableNetworkAgent::class.java.simpleName /* tag */, + nc, lp, TEST_NETWORK_SCORE, conf, Provider(context, looper)) { + private val history = ArrayTrackRecord().newReadHead() + + sealed class CallbackEntry { + object OnBandwidthUpdateRequested : CallbackEntry() + object OnNetworkUnwanted : CallbackEntry() + data class OnAddKeepalivePacketFilter( + val slot: Int, + val packet: KeepalivePacketData + ) : CallbackEntry() + data class OnRemoveKeepalivePacketFilter(val slot: Int) : CallbackEntry() + data class OnStartSocketKeepalive( + val slot: Int, + val interval: Int, + val packet: KeepalivePacketData + ) : CallbackEntry() + data class OnStopSocketKeepalive(val slot: Int) : CallbackEntry() + data class OnSaveAcceptUnvalidated(val accept: Boolean) : CallbackEntry() + object OnAutomaticReconnectDisabled : CallbackEntry() + data class OnValidationStatus(val status: Int, val uri: Uri?) : CallbackEntry() + data class OnSignalStrengthThresholdsUpdated(val thresholds: IntArray) : CallbackEntry() + } + + fun getName(): String? = (nc.getNetworkSpecifier() as? StringNetworkSpecifier)?.specifier + + override fun onBandwidthUpdateRequested() { + history.add(OnBandwidthUpdateRequested) + } + + override fun onNetworkUnwanted() { + history.add(OnNetworkUnwanted) + } + + override fun onAddKeepalivePacketFilter(slot: Int, packet: KeepalivePacketData) { + history.add(OnAddKeepalivePacketFilter(slot, packet)) + } + + override fun onRemoveKeepalivePacketFilter(slot: Int) { + history.add(OnRemoveKeepalivePacketFilter(slot)) + } + + override fun onStartSocketKeepalive( + slot: Int, + interval: Duration, + packet: KeepalivePacketData + ) { + history.add(OnStartSocketKeepalive(slot, interval.seconds.toInt(), packet)) + } + + override fun onStopSocketKeepalive(slot: Int) { + history.add(OnStopSocketKeepalive(slot)) + } + + override fun onSaveAcceptUnvalidated(accept: Boolean) { + history.add(OnSaveAcceptUnvalidated(accept)) + } + + override fun onAutomaticReconnectDisabled() { + history.add(OnAutomaticReconnectDisabled) + } + + override fun onSignalStrengthThresholdsUpdated(thresholds: IntArray) { + history.add(OnSignalStrengthThresholdsUpdated(thresholds)) + } + + fun expectEmptySignalStrengths() { + expectCallback().let { + // intArrayOf() without arguments makes an empty array + assertArrayEquals(intArrayOf(), it.thresholds) + } + } + + override fun onValidationStatus(status: Int, uri: Uri?) { + history.add(OnValidationStatus(status, uri)) + } + + // Expects the initial validation event that always occurs immediately after registering + // a NetworkAgent whose network does not require validation (which test networks do + // not, since they lack the INTERNET capability). It always contains the default argument + // for the URI. + fun expectNoInternetValidationStatus() = expectCallback().let { + assertEquals(it.status, VALID_NETWORK) + // The returned Uri is parsed from the empty string, which means it's an + // instance of the (private) Uri.StringUri. There are no real good ways + // to check this, the least bad is to just convert it to a string and + // make sure it's empty. + assertEquals("", it.uri.toString()) + } + + inline fun expectCallback(): T { + val foundCallback = history.poll(DEFAULT_TIMEOUT_MS) + assertTrue(foundCallback is T, "Expected ${T::class} but found $foundCallback") + return foundCallback + } + + fun assertNoCallback() { + assertTrue(waitForIdle(DEFAULT_TIMEOUT_MS), + "Handler didn't became idle after ${DEFAULT_TIMEOUT_MS}ms") + assertNull(history.peek()) + } + } + + private fun requestNetwork(request: NetworkRequest, callback: TestableNetworkCallback) { + mCM.requestNetwork(request, callback) + callbacksToCleanUp.add(callback) + } + + private fun registerNetworkCallback( + request: NetworkRequest, + callback: TestableNetworkCallback + ) { + mCM.registerNetworkCallback(request, callback) + callbacksToCleanUp.add(callback) + } + + private fun createNetworkAgent( + context: Context = realContext, + name: String? = null + ): TestableNetworkAgent { + 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) + if (null != name) { + setNetworkSpecifier(StringNetworkSpecifier(name)) + } + } + val lp = LinkProperties().apply { + addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 0)) + } + val config = NetworkAgentConfig.Builder().build() + return TestableNetworkAgent(context, mHandlerThread.looper, nc, lp, config).also { + agentsToCleanUp.add(it) + } + } + + private fun createConnectedNetworkAgent(context: Context = realContext, name: String? = null): + Pair { + val request: NetworkRequest = NetworkRequest.Builder() + .clearCapabilities() + .addTransportType(NetworkCapabilities.TRANSPORT_TEST) + .build() + val callback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS) + requestNetwork(request, callback) + val agent = createNetworkAgent(context, name) + agent.register() + agent.markConnected() + return agent to callback + } + + private fun createNetworkAgentWithFakeCS() = createNetworkAgent().also { + mFakeConnectivityService.connect(it.registerForTest(Network(FAKE_NET_ID))) + } + + @Test + fun testConnectAndUnregister() { + val (agent, callback) = createConnectedNetworkAgent() + callback.expectAvailableThenValidatedCallbacks(agent.network) + agent.expectEmptySignalStrengths() + agent.expectNoInternetValidationStatus() + agent.unregister() + callback.expectCallback(agent.network) + agent.expectCallback() + assertFailsWith("Must not be able to register an agent twice") { + agent.register() + } + } + + @Test + fun testOnBandwidthUpdateRequested() { + val (agent, callback) = createConnectedNetworkAgent() + callback.expectAvailableThenValidatedCallbacks(agent.network) + agent.expectEmptySignalStrengths() + agent.expectNoInternetValidationStatus() + mCM.requestBandwidthUpdate(agent.network) + agent.expectCallback() + agent.unregister() + } + + @Test + fun testSignalStrengthThresholds() { + val thresholds = intArrayOf(30, 50, 65) + val callbacks = thresholds.map { strength -> + val request = NetworkRequest.Builder() + .clearCapabilities() + .addTransportType(NetworkCapabilities.TRANSPORT_TEST) + .setSignalStrength(strength) + .build() + TestableNetworkCallback(DEFAULT_TIMEOUT_MS).also { + registerNetworkCallback(request, it) + } + } + createConnectedNetworkAgent().let { (agent, callback) -> + callback.expectAvailableThenValidatedCallbacks(agent.network) + agent.expectCallback().let { + assertArrayEquals(it.thresholds, thresholds) + } + agent.expectNoInternetValidationStatus() + + // Send signal strength and check that the callbacks are called appropriately. + val nc = NetworkCapabilities(agent.nc) + nc.setSignalStrength(20) + agent.sendNetworkCapabilities(nc) + callbacks.forEach { it.assertNoCallback(NO_CALLBACK_TIMEOUT) } + + nc.setSignalStrength(40) + agent.sendNetworkCapabilities(nc) + callbacks[0].expectAvailableCallbacks(agent.network) + callbacks[1].assertNoCallback(NO_CALLBACK_TIMEOUT) + callbacks[2].assertNoCallback(NO_CALLBACK_TIMEOUT) + + nc.setSignalStrength(80) + agent.sendNetworkCapabilities(nc) + callbacks[0].expectCapabilitiesThat(agent.network) { it.signalStrength == 80 } + callbacks[1].expectAvailableCallbacks(agent.network) + callbacks[2].expectAvailableCallbacks(agent.network) + + nc.setSignalStrength(55) + agent.sendNetworkCapabilities(nc) + callbacks[0].expectCapabilitiesThat(agent.network) { it.signalStrength == 55 } + callbacks[1].expectCapabilitiesThat(agent.network) { it.signalStrength == 55 } + callbacks[2].expectCallback(agent.network) + } + callbacks.forEach { + mCM.unregisterNetworkCallback(it) + } + } + + @Test + fun testSocketKeepalive(): Unit = createNetworkAgentWithFakeCS().let { agent -> + val packet = object : KeepalivePacketData( + LOCAL_IPV4_ADDRESS /* srcAddress */, 1234 /* srcPort */, + REMOTE_IPV4_ADDRESS /* dstAddress */, 4567 /* dstPort */, + ByteArray(100 /* size */) { it.toByte() /* init */ }) {} + val slot = 4 + val interval = 37 + + mFakeConnectivityService.sendMessage(CMD_ADD_KEEPALIVE_PACKET_FILTER, + arg1 = slot, obj = packet) + mFakeConnectivityService.sendMessage(CMD_START_SOCKET_KEEPALIVE, + arg1 = slot, arg2 = interval, obj = packet) + + agent.expectCallback().let { + assertEquals(it.slot, slot) + assertEquals(it.packet, packet) + } + agent.expectCallback().let { + assertEquals(it.slot, slot) + assertEquals(it.interval, interval) + assertEquals(it.packet, packet) + } + + agent.assertNoCallback() + + // Check that when the agent sends a keepalive event, ConnectivityService receives the + // expected message. + agent.sendSocketKeepaliveEvent(slot, SocketKeepalive.ERROR_UNSUPPORTED) + mFakeConnectivityService.expectMessage(NetworkAgent.EVENT_SOCKET_KEEPALIVE).let() { + assertEquals(slot, it.arg1) + assertEquals(SocketKeepalive.ERROR_UNSUPPORTED, it.arg2) + } + + mFakeConnectivityService.sendMessage(CMD_STOP_SOCKET_KEEPALIVE, arg1 = slot) + mFakeConnectivityService.sendMessage(CMD_REMOVE_KEEPALIVE_PACKET_FILTER, arg1 = slot) + agent.expectCallback().let { + assertEquals(it.slot, slot) + } + agent.expectCallback().let { + assertEquals(it.slot, slot) + } + } + + @Test + fun testSendUpdates(): Unit = createConnectedNetworkAgent().let { (agent, callback) -> + callback.expectAvailableThenValidatedCallbacks(agent.network) + agent.expectEmptySignalStrengths() + agent.expectNoInternetValidationStatus() + val ifaceName = "adhocIface" + val lp = LinkProperties(agent.lp) + lp.setInterfaceName(ifaceName) + agent.sendLinkProperties(lp) + callback.expectLinkPropertiesThat(agent.network) { + it.getInterfaceName() == ifaceName + } + val nc = NetworkCapabilities(agent.nc) + nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + agent.sendNetworkCapabilities(nc) + callback.expectCapabilitiesThat(agent.network) { + it.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + } + } + + @Test + fun testSendScore() { + // This test will create two networks and check that the one with the stronger + // score wins out for a request that matches them both. + // First create requests to make sure both networks are kept up, using the + // specifier so they are specific to each network + val name1 = UUID.randomUUID().toString() + val name2 = UUID.randomUUID().toString() + val request1 = NetworkRequest.Builder() + .clearCapabilities() + .addTransportType(NetworkCapabilities.TRANSPORT_TEST) + .setNetworkSpecifier(StringNetworkSpecifier(name1)) + .build() + val request2 = NetworkRequest.Builder() + .clearCapabilities() + .addTransportType(NetworkCapabilities.TRANSPORT_TEST) + .setNetworkSpecifier(StringNetworkSpecifier(name2)) + .build() + val callback1 = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS) + val callback2 = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS) + requestNetwork(request1, callback1) + requestNetwork(request2, callback2) + + // Then file the interesting request + val request = NetworkRequest.Builder() + .clearCapabilities() + .addTransportType(NetworkCapabilities.TRANSPORT_TEST) + .build() + val callback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS) + requestNetwork(request, callback) + + // Connect the first Network + createConnectedNetworkAgent(name = name1).let { (agent1, _) -> + callback.expectAvailableThenValidatedCallbacks(agent1.network) + // Upgrade agent1 to a better score so that there is no ambiguity when + // agent2 connects that agent1 is still better + agent1.sendNetworkScore(BETTER_NETWORK_SCORE - 1) + // Connect the second agent + createConnectedNetworkAgent(name = name2).let { (agent2, _) -> + agent2.markConnected() + // The callback should not see anything yet + callback.assertNoCallback(NO_CALLBACK_TIMEOUT) + // Now update the score and expect the callback now prefers agent2 + agent2.sendNetworkScore(BETTER_NETWORK_SCORE) + callback.expectCallback(agent2.network) + } + } + + // tearDown() will unregister the requests and agents + } + + @Test + fun testAgentStartsInConnecting() { + val mockContext = mock(Context::class.java) + val mockCm = mock(ConnectivityManager::class.java) + doReturn(mockCm).`when`(mockContext).getSystemService(Context.CONNECTIVITY_SERVICE) + createConnectedNetworkAgent(mockContext) + verify(mockCm).registerNetworkAgent(any(Messenger::class.java), + argThat { it.detailedState == NetworkInfo.DetailedState.CONNECTING }, + any(LinkProperties::class.java), + any(NetworkCapabilities::class.java), + anyInt() /* score */, + any(NetworkAgentConfig::class.java), + eq(NetworkProvider.ID_NONE)) + } + + @Test + fun testSetAcceptUnvalidated() { + createNetworkAgentWithFakeCS().let { agent -> + mFakeConnectivityService.sendMessage(CMD_SAVE_ACCEPT_UNVALIDATED, 1) + agent.expectCallback().let { + assertTrue(it.accept) + } + agent.assertNoCallback() + } + } + + @Test + fun testSetAcceptUnvalidatedPreventAutomaticReconnect() { + createNetworkAgentWithFakeCS().let { agent -> + mFakeConnectivityService.sendMessage(CMD_SAVE_ACCEPT_UNVALIDATED, 0) + mFakeConnectivityService.sendMessage(CMD_PREVENT_AUTOMATIC_RECONNECT) + agent.expectCallback().let { + assertFalse(it.accept) + } + agent.expectCallback() + agent.assertNoCallback() + // When automatic reconnect is turned off, the network is torn down and + // ConnectivityService sends a disconnect. This in turn causes the agent + // to send a DISCONNECTED message to CS. + mFakeConnectivityService.willExpectDisconnectOnce() + mFakeConnectivityService.disconnect() + mFakeConnectivityService.expectMessage(AsyncChannel.CMD_CHANNEL_DISCONNECTED) + agent.expectCallback() + } + } + + @Test + fun testPreventAutomaticReconnect() { + createNetworkAgentWithFakeCS().let { agent -> + mFakeConnectivityService.sendMessage(CMD_PREVENT_AUTOMATIC_RECONNECT) + agent.expectCallback() + agent.assertNoCallback() + mFakeConnectivityService.willExpectDisconnectOnce() + mFakeConnectivityService.disconnect() + mFakeConnectivityService.expectMessage(AsyncChannel.CMD_CHANNEL_DISCONNECTED) + agent.expectCallback() + } + } + + @Test + fun testValidationStatus() = createNetworkAgentWithFakeCS().let { agent -> + val uri = Uri.parse("http://www.google.com") + val bundle = Bundle().apply { + putString(NetworkAgent.REDIRECT_URL_KEY, uri.toString()) + } + mFakeConnectivityService.sendMessage(CMD_REPORT_NETWORK_STATUS, + arg1 = VALID_NETWORK, obj = bundle) + agent.expectCallback().let { + assertEquals(it.status, VALID_NETWORK) + assertEquals(it.uri, uri) + } + + mFakeConnectivityService.sendMessage(CMD_REPORT_NETWORK_STATUS, + arg1 = INVALID_NETWORK, obj = Bundle()) + agent.expectCallback().let { + assertEquals(it.status, INVALID_NETWORK) + assertNull(it.uri) + } + } + + @Test + fun testTemporarilyUnmeteredCapability() { + // This test will create a networks with/without NET_CAPABILITY_TEMPORARILY_NOT_METERED + // and check that the callback reflects the capability changes. + // First create a request to make sure the network is kept up + val request1 = NetworkRequest.Builder() + .clearCapabilities() + .addTransportType(NetworkCapabilities.TRANSPORT_TEST) + .build() + val callback1 = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS).also { + registerNetworkCallback(request1, it) + } + requestNetwork(request1, callback1) + + // Then file the interesting request + val request = NetworkRequest.Builder() + .clearCapabilities() + .addTransportType(NetworkCapabilities.TRANSPORT_TEST) + .build() + val callback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS) + requestNetwork(request, callback) + + // Connect the network + createConnectedNetworkAgent().let { (agent, _) -> + callback.expectAvailableThenValidatedCallbacks(agent.network) + + // Send TEMP_NOT_METERED and check that the callback is called appropriately. + val nc1 = NetworkCapabilities(agent.nc) + .addCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED) + agent.sendNetworkCapabilities(nc1) + callback.expectCapabilitiesThat(agent.network) { + it.hasCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED) + } + + // Remove TEMP_NOT_METERED and check that the callback is called appropriately. + val nc2 = NetworkCapabilities(agent.nc) + .removeCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED) + agent.sendNetworkCapabilities(nc2) + callback.expectCapabilitiesThat(agent.network) { + !it.hasCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED) + } + } + + // tearDown() will unregister the requests and agents + } +} diff --git a/tests/cts/net/src/android/net/cts/NetworkInfoTest.kt b/tests/cts/net/src/android/net/cts/NetworkInfoTest.kt new file mode 100644 index 0000000000..fa15e8f82c --- /dev/null +++ b/tests/cts/net/src/android/net/cts/NetworkInfoTest.kt @@ -0,0 +1,122 @@ +/* + * 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.cts + +import android.os.Build +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkInfo +import android.net.NetworkInfo.DetailedState +import android.net.NetworkInfo.State +import android.telephony.TelephonyManager +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +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.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Rule +import org.junit.runner.RunWith +import org.junit.Test + +const val TYPE_MOBILE = ConnectivityManager.TYPE_MOBILE +const val TYPE_WIFI = ConnectivityManager.TYPE_WIFI +const val MOBILE_TYPE_NAME = "mobile" +const val WIFI_TYPE_NAME = "WIFI" +const val LTE_SUBTYPE_NAME = "LTE" + +@SmallTest +@RunWith(AndroidJUnit4::class) +class NetworkInfoTest { + @Rule @JvmField + val ignoreRule = DevSdkIgnoreRule() + + @Test + fun testAccessNetworkInfoProperties() { + val cm = InstrumentationRegistry.getInstrumentation().context + .getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val ni = cm.getAllNetworkInfo() + assertTrue(ni.isNotEmpty()) + + for (netInfo in ni) { + when (netInfo.getType()) { + TYPE_MOBILE -> assertNetworkInfo(netInfo, MOBILE_TYPE_NAME) + TYPE_WIFI -> assertNetworkInfo(netInfo, WIFI_TYPE_NAME) + // TODO: Add BLUETOOTH_TETHER testing + } + } + } + + private fun assertNetworkInfo(netInfo: NetworkInfo, expectedTypeName: String) { + assertTrue(expectedTypeName.equals(netInfo.getTypeName(), ignoreCase = true)) + assertNotNull(netInfo.toString()) + + if (!netInfo.isConnectedOrConnecting()) return + + assertTrue(netInfo.isAvailable()) + if (State.CONNECTED == netInfo.getState()) { + assertTrue(netInfo.isConnected()) + } + assertTrue(State.CONNECTING == netInfo.getState() || + State.CONNECTED == netInfo.getState()) + assertTrue(DetailedState.SCANNING == netInfo.getDetailedState() || + DetailedState.CONNECTING == netInfo.getDetailedState() || + DetailedState.AUTHENTICATING == netInfo.getDetailedState() || + DetailedState.CONNECTED == netInfo.getDetailedState()) + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testConstructor() { + val networkInfo = NetworkInfo(TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_LTE, + MOBILE_TYPE_NAME, LTE_SUBTYPE_NAME) + + assertEquals(TYPE_MOBILE, networkInfo.type) + assertEquals(TelephonyManager.NETWORK_TYPE_LTE, networkInfo.subtype) + assertEquals(MOBILE_TYPE_NAME, networkInfo.typeName) + assertEquals(LTE_SUBTYPE_NAME, networkInfo.subtypeName) + assertEquals(DetailedState.IDLE, networkInfo.detailedState) + assertEquals(State.UNKNOWN, networkInfo.state) + assertNull(networkInfo.reason) + assertNull(networkInfo.extraInfo) + + try { + NetworkInfo(ConnectivityManager.MAX_NETWORK_TYPE + 1, + TelephonyManager.NETWORK_TYPE_LTE, MOBILE_TYPE_NAME, LTE_SUBTYPE_NAME) + fail("Unexpected behavior. Network type is invalid.") + } catch (e: IllegalArgumentException) { + // Expected behavior. + } + } + + @Test + fun testSetDetailedState() { + val networkInfo = NetworkInfo(TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_LTE, + MOBILE_TYPE_NAME, LTE_SUBTYPE_NAME) + val reason = "TestNetworkInfo" + val extraReason = "setDetailedState test" + + networkInfo.setDetailedState(DetailedState.CONNECTED, reason, extraReason) + assertEquals(DetailedState.CONNECTED, networkInfo.detailedState) + assertEquals(State.CONNECTED, networkInfo.state) + assertEquals(reason, networkInfo.reason) + assertEquals(extraReason, networkInfo.extraInfo) + } +} diff --git a/tests/cts/net/src/android/net/cts/NetworkInfo_DetailedStateTest.java b/tests/cts/net/src/android/net/cts/NetworkInfo_DetailedStateTest.java new file mode 100644 index 0000000000..590ce89579 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/NetworkInfo_DetailedStateTest.java @@ -0,0 +1,56 @@ +/* + * 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.cts; + + +import android.net.NetworkInfo.DetailedState; +import android.test.AndroidTestCase; + +public class NetworkInfo_DetailedStateTest extends AndroidTestCase { + + public void testValueOf() { + assertEquals(DetailedState.AUTHENTICATING, DetailedState.valueOf("AUTHENTICATING")); + assertEquals(DetailedState.CONNECTED, DetailedState.valueOf("CONNECTED")); + assertEquals(DetailedState.CONNECTING, DetailedState.valueOf("CONNECTING")); + assertEquals(DetailedState.DISCONNECTED, DetailedState.valueOf("DISCONNECTED")); + assertEquals(DetailedState.DISCONNECTING, DetailedState.valueOf("DISCONNECTING")); + assertEquals(DetailedState.FAILED, DetailedState.valueOf("FAILED")); + assertEquals(DetailedState.IDLE, DetailedState.valueOf("IDLE")); + assertEquals(DetailedState.OBTAINING_IPADDR, DetailedState.valueOf("OBTAINING_IPADDR")); + assertEquals(DetailedState.SCANNING, DetailedState.valueOf("SCANNING")); + assertEquals(DetailedState.SUSPENDED, DetailedState.valueOf("SUSPENDED")); + } + + public void testValues() { + DetailedState[] expected = DetailedState.values(); + assertEquals(13, expected.length); + assertEquals(DetailedState.IDLE, expected[0]); + assertEquals(DetailedState.SCANNING, expected[1]); + assertEquals(DetailedState.CONNECTING, expected[2]); + assertEquals(DetailedState.AUTHENTICATING, expected[3]); + assertEquals(DetailedState.OBTAINING_IPADDR, expected[4]); + assertEquals(DetailedState.CONNECTED, expected[5]); + assertEquals(DetailedState.SUSPENDED, expected[6]); + assertEquals(DetailedState.DISCONNECTING, expected[7]); + assertEquals(DetailedState.DISCONNECTED, expected[8]); + assertEquals(DetailedState.FAILED, expected[9]); + assertEquals(DetailedState.BLOCKED, expected[10]); + assertEquals(DetailedState.VERIFYING_POOR_LINK, expected[11]); + assertEquals(DetailedState.CAPTIVE_PORTAL_CHECK, expected[12]); + } + +} diff --git a/tests/cts/net/src/android/net/cts/NetworkInfo_StateTest.java b/tests/cts/net/src/android/net/cts/NetworkInfo_StateTest.java new file mode 100644 index 0000000000..5303ef1281 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/NetworkInfo_StateTest.java @@ -0,0 +1,43 @@ +/* + * 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.cts; + +import android.net.NetworkInfo.State; +import android.test.AndroidTestCase; + +public class NetworkInfo_StateTest extends AndroidTestCase { + + public void testValueOf() { + assertEquals(State.CONNECTED, State.valueOf("CONNECTED")); + assertEquals(State.CONNECTING, State.valueOf("CONNECTING")); + assertEquals(State.DISCONNECTED, State.valueOf("DISCONNECTED")); + assertEquals(State.DISCONNECTING, State.valueOf("DISCONNECTING")); + assertEquals(State.SUSPENDED, State.valueOf("SUSPENDED")); + assertEquals(State.UNKNOWN, State.valueOf("UNKNOWN")); + } + + public void testValues() { + State[] expected = State.values(); + assertEquals(6, expected.length); + assertEquals(State.CONNECTING, expected[0]); + assertEquals(State.CONNECTED, expected[1]); + assertEquals(State.SUSPENDED, expected[2]); + assertEquals(State.DISCONNECTING, expected[3]); + assertEquals(State.DISCONNECTED, expected[4]); + assertEquals(State.UNKNOWN, expected[5]); + } +} diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java new file mode 100644 index 0000000000..d118c8a0ca --- /dev/null +++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java @@ -0,0 +1,276 @@ +/* + * 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.cts; + +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED; +import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; + +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 android.net.MacAddress; +import android.net.MatchAllNetworkSpecifier; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.NetworkSpecifier; +import android.net.UidRange; +import android.net.wifi.WifiNetworkSpecifier; +import android.os.Build; +import android.os.PatternMatcher; +import android.os.Process; +import android.util.ArraySet; + +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) +public class NetworkRequestTest { + @Rule + public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(); + + private static final String TEST_SSID = "TestSSID"; + private static final String OTHER_SSID = "OtherSSID"; + private static final int TEST_UID = 2097; + private static final String TEST_PACKAGE_NAME = "test.package.name"; + private static final MacAddress ARBITRARY_ADDRESS = MacAddress.fromString("3:5:8:12:9:2"); + + private class LocalNetworkSpecifier extends NetworkSpecifier { + private final int mId; + + LocalNetworkSpecifier(int id) { + mId = id; + } + + @Override + public boolean canBeSatisfiedBy(NetworkSpecifier other) { + return other instanceof LocalNetworkSpecifier + && mId == ((LocalNetworkSpecifier) other).mId; + } + } + + @Test + public void testCapabilities() { + assertTrue(new NetworkRequest.Builder().addCapability(NET_CAPABILITY_MMS).build() + .hasCapability(NET_CAPABILITY_MMS)); + assertFalse(new NetworkRequest.Builder().removeCapability(NET_CAPABILITY_MMS).build() + .hasCapability(NET_CAPABILITY_MMS)); + + final NetworkRequest nr = new NetworkRequest.Builder().clearCapabilities().build(); + // Verify request has no capabilities + verifyNoCapabilities(nr); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testTemporarilyNotMeteredCapability() { + assertTrue(new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED).build() + .hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)); + assertFalse(new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED).build() + .hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)); + } + + private void verifyNoCapabilities(NetworkRequest nr) { + // NetworkCapabilities.mNetworkCapabilities is defined as type long + final int MAX_POSSIBLE_CAPABILITY = Long.SIZE; + for(int bit = 0; bit < MAX_POSSIBLE_CAPABILITY; bit++) { + assertFalse(nr.hasCapability(bit)); + } + } + + @Test + public void testTransports() { + assertTrue(new NetworkRequest.Builder().addTransportType(TRANSPORT_BLUETOOTH).build() + .hasTransport(TRANSPORT_BLUETOOTH)); + assertFalse(new NetworkRequest.Builder().removeTransportType(TRANSPORT_BLUETOOTH).build() + .hasTransport(TRANSPORT_BLUETOOTH)); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testSpecifier() { + assertNull(new NetworkRequest.Builder().build().getNetworkSpecifier()); + final WifiNetworkSpecifier specifier = new WifiNetworkSpecifier.Builder() + .setSsidPattern(new PatternMatcher(TEST_SSID, PatternMatcher.PATTERN_LITERAL)) + .setBssidPattern(ARBITRARY_ADDRESS, ARBITRARY_ADDRESS) + .build(); + final NetworkSpecifier obtainedSpecifier = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI) + .setNetworkSpecifier(specifier) + .build() + .getNetworkSpecifier(); + assertEquals(obtainedSpecifier, specifier); + + assertNull(new NetworkRequest.Builder() + .clearCapabilities() + .build() + .getNetworkSpecifier()); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testRequestorPackageName() { + assertNull(new NetworkRequest.Builder().build().getRequestorPackageName()); + final String pkgName = "android.net.test"; + final NetworkCapabilities nc = new NetworkCapabilities.Builder() + .setRequestorPackageName(pkgName) + .build(); + final NetworkRequest nr = new NetworkRequest.Builder() + .setCapabilities(nc) + .build(); + assertEquals(pkgName, nr.getRequestorPackageName()); + assertNull(new NetworkRequest.Builder() + .clearCapabilities() + .build() + .getRequestorPackageName()); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testCanBeSatisfiedBy() { + final LocalNetworkSpecifier specifier1 = new LocalNetworkSpecifier(1234 /* id */); + final LocalNetworkSpecifier specifier2 = new LocalNetworkSpecifier(5678 /* id */); + + final NetworkCapabilities capCellularMmsInternet = new NetworkCapabilities() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_MMS) + .addCapability(NET_CAPABILITY_INTERNET); + final NetworkCapabilities capCellularVpnMmsInternet = + new NetworkCapabilities(capCellularMmsInternet).addTransportType(TRANSPORT_VPN); + final NetworkCapabilities capCellularMmsInternetSpecifier1 = + new NetworkCapabilities(capCellularMmsInternet).setNetworkSpecifier(specifier1); + final NetworkCapabilities capVpnInternetSpecifier1 = new NetworkCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .addTransportType(TRANSPORT_VPN) + .setNetworkSpecifier(specifier1); + final NetworkCapabilities capCellularMmsInternetMatchallspecifier = + new NetworkCapabilities(capCellularMmsInternet) + .setNetworkSpecifier(new MatchAllNetworkSpecifier()); + final NetworkCapabilities capCellularMmsInternetSpecifier2 = + new NetworkCapabilities(capCellularMmsInternet).setNetworkSpecifier(specifier2); + + final NetworkRequest requestCellularInternetSpecifier1 = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_INTERNET) + .setNetworkSpecifier(specifier1) + .build(); + assertFalse(requestCellularInternetSpecifier1.canBeSatisfiedBy(null)); + assertFalse(requestCellularInternetSpecifier1.canBeSatisfiedBy(new NetworkCapabilities())); + assertTrue(requestCellularInternetSpecifier1.canBeSatisfiedBy( + capCellularMmsInternetMatchallspecifier)); + assertFalse(requestCellularInternetSpecifier1.canBeSatisfiedBy(capCellularMmsInternet)); + assertTrue(requestCellularInternetSpecifier1.canBeSatisfiedBy( + capCellularMmsInternetSpecifier1)); + assertFalse(requestCellularInternetSpecifier1.canBeSatisfiedBy(capCellularVpnMmsInternet)); + assertFalse(requestCellularInternetSpecifier1.canBeSatisfiedBy( + capCellularMmsInternetSpecifier2)); + + final NetworkRequest requestCellularInternet = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_INTERNET) + .build(); + assertTrue(requestCellularInternet.canBeSatisfiedBy(capCellularMmsInternet)); + assertTrue(requestCellularInternet.canBeSatisfiedBy(capCellularMmsInternetSpecifier1)); + assertTrue(requestCellularInternet.canBeSatisfiedBy(capCellularMmsInternetSpecifier2)); + assertFalse(requestCellularInternet.canBeSatisfiedBy(capVpnInternetSpecifier1)); + assertTrue(requestCellularInternet.canBeSatisfiedBy(capCellularVpnMmsInternet)); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testInvariantInCanBeSatisfiedBy() { + // Test invariant that result of NetworkRequest.canBeSatisfiedBy() should be the same with + // NetworkCapabilities.satisfiedByNetworkCapabilities(). + final LocalNetworkSpecifier specifier1 = new LocalNetworkSpecifier(1234 /* id */); + final int uid = Process.myUid(); + final ArraySet ranges = new ArraySet<>(); + ranges.add(new UidRange(uid, uid)); + final NetworkRequest requestCombination = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_INTERNET) + .setLinkUpstreamBandwidthKbps(1000) + .setNetworkSpecifier(specifier1) + .setSignalStrength(-123) + .setUids(ranges).build(); + final NetworkCapabilities capCell = new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + assertCorrectlySatisfies(false, requestCombination, capCell); + + final NetworkCapabilities capCellInternet = new NetworkCapabilities.Builder(capCell) + .addCapability(NET_CAPABILITY_INTERNET).build(); + assertCorrectlySatisfies(false, requestCombination, capCellInternet); + + final NetworkCapabilities capCellInternetBW = + new NetworkCapabilities.Builder(capCellInternet) + .setLinkUpstreamBandwidthKbps(1024).build(); + assertCorrectlySatisfies(false, requestCombination, capCellInternetBW); + + final NetworkCapabilities capCellInternetBWSpecifier1 = + new NetworkCapabilities.Builder(capCellInternetBW) + .setNetworkSpecifier(specifier1).build(); + assertCorrectlySatisfies(false, requestCombination, capCellInternetBWSpecifier1); + + final NetworkCapabilities capCellInternetBWSpecifier1Signal = + new NetworkCapabilities.Builder(capCellInternetBWSpecifier1) + .setSignalStrength(-123).build(); + assertCorrectlySatisfies(true, requestCombination, + capCellInternetBWSpecifier1Signal); + + final NetworkCapabilities capCellInternetBWSpecifier1SignalUid = + new NetworkCapabilities.Builder(capCellInternetBWSpecifier1Signal) + .setOwnerUid(uid) + .setAdministratorUids(new int [] {uid}).build(); + assertCorrectlySatisfies(true, requestCombination, + capCellInternetBWSpecifier1SignalUid); + } + + private void assertCorrectlySatisfies(boolean expect, NetworkRequest request, + NetworkCapabilities nc) { + assertEquals(expect, request.canBeSatisfiedBy(nc)); + assertEquals( + request.canBeSatisfiedBy(nc), + request.networkCapabilities.satisfiedByNetworkCapabilities(nc)); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testRequestorUid() { + final NetworkCapabilities nc = new NetworkCapabilities(); + // Verify default value is INVALID_UID + assertEquals(Process.INVALID_UID, new NetworkRequest.Builder() + .setCapabilities(nc).build().getRequestorUid()); + + nc.setRequestorUid(1314); + final NetworkRequest nr = new NetworkRequest.Builder().setCapabilities(nc).build(); + assertEquals(1314, nr.getRequestorUid()); + + assertEquals(Process.INVALID_UID, new NetworkRequest.Builder() + .clearCapabilities().build().getRequestorUid()); + } +} diff --git a/tests/cts/net/src/android/net/cts/NetworkStackDependenciesTest.kt b/tests/cts/net/src/android/net/cts/NetworkStackDependenciesTest.kt new file mode 100644 index 0000000000..1a7f9555f6 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/NetworkStackDependenciesTest.kt @@ -0,0 +1,53 @@ +/* + * 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.cts + +import android.content.pm.PackageManager +import android.net.cts.util.CtsNetUtils +import android.net.wifi.WifiManager +import android.os.Build +import androidx.test.filters.SdkSuppress +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assume.assumeTrue +import org.junit.Test +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * Basic tests for APIs used by the network stack module. + */ +class NetworkStackDependenciesTest { + @Test + @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.Q) + fun testGetFrequency() { + // WifiInfo#getFrequency was missing a CTS test in Q: this test is run as part of MTS on Q + // devices to ensure it behaves correctly. + val context = InstrumentationRegistry.getInstrumentation().getContext() + assumeTrue("This test only applies to devices that support wifi", + context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI)) + val wifiManager = context.getSystemService(WifiManager::class.java) + assertNotNull(wifiManager, "Device supports wifi but there is no WifiManager") + + CtsNetUtils(context).ensureWifiConnected() + val wifiInfo = wifiManager.getConnectionInfo() + // The NetworkStack can handle any value of getFrequency; unknown frequencies will not be + // classified in metrics, but this is expected behavior. It is only important that the + // method does not crash. Still verify that the frequency is positive + val frequency = wifiInfo.getFrequency() + assertTrue(frequency > 0, "Frequency must be > 0") + } +} \ No newline at end of file diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java new file mode 100644 index 0000000000..1a48983028 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java @@ -0,0 +1,146 @@ +/* + * 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.cts; + +import static android.os.Process.INVALID_UID; + +import static org.junit.Assert.assertEquals; + +import android.annotation.NonNull; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.net.INetworkStatsService; +import android.net.TrafficStats; +import android.os.Build; +import android.os.IBinder; +import android.os.Process; +import android.os.RemoteException; +import android.test.AndroidTestCase; +import android.util.SparseArray; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.CollectionUtils; +import com.android.testutils.DevSdkIgnoreRule; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; + +@RunWith(AndroidJUnit4.class) +public class NetworkStatsBinderTest { + // NOTE: These are shamelessly copied from TrafficStats. + private static final int TYPE_RX_BYTES = 0; + private static final int TYPE_RX_PACKETS = 1; + private static final int TYPE_TX_BYTES = 2; + private static final int TYPE_TX_PACKETS = 3; + + @Rule + public DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule( + Build.VERSION_CODES.Q /* ignoreClassUpTo */); + + private final SparseArray> mUidStatsQueryOpArray = new SparseArray<>(); + + @Before + public void setUp() throws Exception { + mUidStatsQueryOpArray.put(TYPE_RX_BYTES, uid -> TrafficStats.getUidRxBytes(uid)); + mUidStatsQueryOpArray.put(TYPE_RX_PACKETS, uid -> TrafficStats.getUidRxPackets(uid)); + mUidStatsQueryOpArray.put(TYPE_TX_BYTES, uid -> TrafficStats.getUidTxBytes(uid)); + mUidStatsQueryOpArray.put(TYPE_TX_PACKETS, uid -> TrafficStats.getUidTxPackets(uid)); + } + + private long getUidStatsFromBinder(int uid, int type) throws Exception { + Method getServiceMethod = Class.forName("android.os.ServiceManager") + .getDeclaredMethod("getService", new Class[]{String.class}); + IBinder binder = (IBinder) getServiceMethod.invoke(null, Context.NETWORK_STATS_SERVICE); + INetworkStatsService nss = INetworkStatsService.Stub.asInterface(binder); + return nss.getUidStats(uid, type); + } + + private int getFirstAppUidThat(@NonNull Predicate predicate) { + PackageManager pm = InstrumentationRegistry.getContext().getPackageManager(); + List apps = pm.getInstalledPackages(0 /* flags */); + final PackageInfo match = CollectionUtils.find(apps, + it -> it.applicationInfo != null && predicate.test(it.applicationInfo.uid)); + if (match != null) return match.applicationInfo.uid; + return INVALID_UID; + } + + @Test + public void testAccessUidStatsFromBinder() throws Exception { + final int myUid = Process.myUid(); + final List testUidList = new ArrayList<>(); + + // Prepare uid list for testing. + testUidList.add(INVALID_UID); + testUidList.add(Process.ROOT_UID); + testUidList.add(Process.SYSTEM_UID); + testUidList.add(myUid); + testUidList.add(Process.LAST_APPLICATION_UID); + testUidList.add(Process.LAST_APPLICATION_UID + 1); + // If available, pick another existing uid for testing that is not already contained + // in the list above. + final int notMyUid = getFirstAppUidThat(uid -> uid >= 0 && !testUidList.contains(uid)); + if (notMyUid != INVALID_UID) testUidList.add(notMyUid); + + for (final int uid : testUidList) { + for (int i = 0; i < mUidStatsQueryOpArray.size(); i++) { + final int type = mUidStatsQueryOpArray.keyAt(i); + try { + final long uidStatsFromBinder = getUidStatsFromBinder(uid, type); + final long uidTrafficStats = mUidStatsQueryOpArray.get(type).apply(uid); + + // Verify that UNSUPPORTED is returned if the uid is not current app uid. + if (uid != myUid) { + assertEquals(uidStatsFromBinder, TrafficStats.UNSUPPORTED); + } + // Verify that returned result is the same with the result get from + // TrafficStats. + // TODO: If the test is flaky then it should instead assert that the values + // are approximately similar. + assertEquals("uidStats is not matched for query type " + type + + ", uid=" + uid + ", myUid=" + myUid, uidTrafficStats, + uidStatsFromBinder); + } catch (IllegalAccessException e) { + /* Java language access prevents exploitation. */ + return; + } catch (InvocationTargetException e) { + /* Underlying method has been changed. */ + return; + } catch (ClassNotFoundException e) { + /* not vulnerable if hidden API no longer available */ + return; + } catch (NoSuchMethodException e) { + /* not vulnerable if hidden API no longer available */ + return; + } catch (RemoteException e) { + return; + } + } + } + } +} diff --git a/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt b/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt new file mode 100644 index 0000000000..5290f0db28 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt @@ -0,0 +1,245 @@ +/* + * 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.cts + +import android.Manifest.permission.MANAGE_TEST_NETWORKS +import android.Manifest.permission.NETWORK_SETTINGS +import android.content.Context +import android.content.pm.PackageManager +import android.net.ConnectivityManager +import android.net.EthernetManager +import android.net.InetAddresses +import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL +import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED +import android.net.NetworkCapabilities.TRANSPORT_ETHERNET +import android.net.NetworkCapabilities.TRANSPORT_TEST +import android.net.NetworkRequest +import android.net.TestNetworkInterface +import android.net.TestNetworkManager +import android.net.Uri +import android.net.dhcp.DhcpDiscoverPacket +import android.net.dhcp.DhcpPacket +import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE +import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_DISCOVER +import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_REQUEST +import android.net.dhcp.DhcpRequestPacket +import android.os.Build +import android.os.HandlerThread +import android.platform.test.annotations.AppModeFull +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.runner.AndroidJUnit4 +import com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress +import com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address +import com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DhcpClientPacketFilter +import com.android.testutils.DhcpOptionFilter +import com.android.testutils.RecorderCallback.CallbackEntry +import com.android.testutils.TapPacketReader +import com.android.testutils.TestHttpServer +import com.android.testutils.TestableNetworkCallback +import com.android.testutils.runAsShell +import fi.iki.elonen.NanoHTTPD.Response.Status +import org.junit.After +import org.junit.Assume.assumeFalse +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.net.Inet4Address +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.test.fail + +private const val MAX_PACKET_LENGTH = 1500 +private const val TEST_TIMEOUT_MS = 10_000L + +private const val TEST_LEASE_TIMEOUT_SECS = 3600 * 12 +private const val TEST_PREFIX_LENGTH = 24 + +private const val TEST_LOGIN_URL = "https://login.capport.android.com" +private const val TEST_VENUE_INFO_URL = "https://venueinfo.capport.android.com" +private const val TEST_DOMAIN_NAME = "lan" +private const val TEST_MTU = 1500.toShort() + +@AppModeFull(reason = "Instant apps cannot create test networks") +@RunWith(AndroidJUnit4::class) +class NetworkValidationTest { + @JvmField + @Rule + val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q) + + private val context by lazy { InstrumentationRegistry.getInstrumentation().context } + private val tnm by lazy { context.assertHasService(TestNetworkManager::class.java) } + private val eth by lazy { context.assertHasService(EthernetManager::class.java) } + private val cm by lazy { context.assertHasService(ConnectivityManager::class.java) } + + private val handlerThread = HandlerThread(NetworkValidationTest::class.java.simpleName) + private val serverIpAddr = InetAddresses.parseNumericAddress("192.0.2.222") as Inet4Address + private val clientIpAddr = InetAddresses.parseNumericAddress("192.0.2.111") as Inet4Address + private val httpServer = TestHttpServer() + private val ethRequest = NetworkRequest.Builder() + // ETHERNET|TEST transport networks do not have NET_CAPABILITY_TRUSTED + .removeCapability(NET_CAPABILITY_TRUSTED) + .addTransportType(TRANSPORT_ETHERNET) + .addTransportType(TRANSPORT_TEST).build() + private val ethRequestCb = TestableNetworkCallback() + + private lateinit var iface: TestNetworkInterface + private lateinit var reader: TapPacketReader + private lateinit var capportUrl: Uri + + private var testSkipped = false + + @Before + fun setUp() { + // This test requires using a tap interface as an ethernet interface. + val pm = context.getPackageManager() + testSkipped = !pm.hasSystemFeature(PackageManager.FEATURE_ETHERNET) && + context.getSystemService(EthernetManager::class.java) == null + assumeFalse(testSkipped) + + // Register a request so the network does not get torn down + cm.requestNetwork(ethRequest, ethRequestCb) + runAsShell(NETWORK_SETTINGS, MANAGE_TEST_NETWORKS) { + eth.setIncludeTestInterfaces(true) + // Keeping a reference to the test interface also makes sure the ParcelFileDescriptor + // does not go out of scope, which would cause it to close the underlying FileDescriptor + // in its finalizer. + iface = tnm.createTapInterface() + } + + handlerThread.start() + reader = TapPacketReader( + handlerThread.threadHandler, + iface.fileDescriptor.fileDescriptor, + MAX_PACKET_LENGTH) + reader.startAsyncForTest() + httpServer.start() + + // Pad the listening port to make sure it is always of length 5. This ensures the URL has + // always the same length so the test can use constant IP and UDP header lengths. + // The maximum port number is 65535 so a length of 5 is always enough. + capportUrl = Uri.parse("http://localhost:${httpServer.listeningPort}/testapi.html?par=val") + } + + @After + fun tearDown() { + if (testSkipped) return + cm.unregisterNetworkCallback(ethRequestCb) + + runAsShell(NETWORK_SETTINGS) { eth.setIncludeTestInterfaces(false) } + + httpServer.stop() + handlerThread.threadHandler.post { reader.stop() } + handlerThread.quitSafely() + + iface.fileDescriptor.close() + } + + @Test + fun testCapportApiCallbacks() { + httpServer.addResponse(capportUrl, Status.OK, content = """ + |{ + | "captive": true, + | "user-portal-url": "$TEST_LOGIN_URL", + | "venue-info-url": "$TEST_VENUE_INFO_URL" + |} + """.trimMargin()) + + // Handle the DHCP handshake that includes the capport API URL + val discover = reader.assertDhcpPacketReceived( + DhcpDiscoverPacket::class.java, TEST_TIMEOUT_MS, DHCP_MESSAGE_TYPE_DISCOVER) + reader.sendResponse(makeOfferPacket(discover.clientMac, discover.transactionId)) + + val request = reader.assertDhcpPacketReceived( + DhcpRequestPacket::class.java, TEST_TIMEOUT_MS, DHCP_MESSAGE_TYPE_REQUEST) + assertEquals(discover.transactionId, request.transactionId) + assertEquals(clientIpAddr, request.mRequestedIp) + reader.sendResponse(makeAckPacket(request.clientMac, request.transactionId)) + + // The first request received by the server should be for the portal API + assertTrue(httpServer.requestsRecord.poll(TEST_TIMEOUT_MS, 0)?.matches(capportUrl) ?: false, + "The device did not fetch captive portal API data within timeout") + + // Expect network callbacks with capport info + val testCb = TestableNetworkCallback(TEST_TIMEOUT_MS) + // LinkProperties do not contain captive portal info if the callback is registered without + // NETWORK_SETTINGS permissions. + val lp = runAsShell(NETWORK_SETTINGS) { + cm.registerNetworkCallback(ethRequest, testCb) + + try { + val ncCb = testCb.eventuallyExpect { + it.caps.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL) + } + testCb.eventuallyExpect { + it.network == ncCb.network && it.lp.captivePortalData != null + }.lp + } finally { + cm.unregisterNetworkCallback(testCb) + } + } + + assertEquals(capportUrl, lp.captivePortalApiUrl) + with(lp.captivePortalData) { + assertNotNull(this) + assertTrue(isCaptive) + assertEquals(Uri.parse(TEST_LOGIN_URL), userPortalUrl) + assertEquals(Uri.parse(TEST_VENUE_INFO_URL), venueInfoUrl) + } + } + + private fun makeOfferPacket(clientMac: ByteArray, transactionId: Int) = + DhcpPacket.buildOfferPacket(DhcpPacket.ENCAP_L2, transactionId, + false /* broadcast */, serverIpAddr, IPV4_ADDR_ANY /* relayIp */, clientIpAddr, + clientMac, TEST_LEASE_TIMEOUT_SECS, + getPrefixMaskAsInet4Address(TEST_PREFIX_LENGTH), + getBroadcastAddress(clientIpAddr, TEST_PREFIX_LENGTH), + listOf(serverIpAddr) /* gateways */, listOf(serverIpAddr) /* dnsServers */, + serverIpAddr, TEST_DOMAIN_NAME, null /* hostname */, true /* metered */, + TEST_MTU, capportUrl.toString()) + + private fun makeAckPacket(clientMac: ByteArray, transactionId: Int) = + DhcpPacket.buildAckPacket(DhcpPacket.ENCAP_L2, transactionId, + false /* broadcast */, serverIpAddr, IPV4_ADDR_ANY /* relayIp */, clientIpAddr, + clientIpAddr /* requestClientIp */, clientMac, TEST_LEASE_TIMEOUT_SECS, + getPrefixMaskAsInet4Address(TEST_PREFIX_LENGTH), + getBroadcastAddress(clientIpAddr, TEST_PREFIX_LENGTH), + listOf(serverIpAddr) /* gateways */, listOf(serverIpAddr) /* dnsServers */, + serverIpAddr, TEST_DOMAIN_NAME, null /* hostname */, true /* metered */, + TEST_MTU, false /* rapidCommit */, capportUrl.toString()) +} + +private fun TapPacketReader.assertDhcpPacketReceived( + packetType: Class, + timeoutMs: Long, + type: Byte +): T { + val packetBytes = poll(timeoutMs, DhcpClientPacketFilter() + .and(DhcpOptionFilter(DHCP_MESSAGE_TYPE, type))) + ?: fail("${packetType.simpleName} not received within timeout") + val packet = DhcpPacket.decodeFullPacket(packetBytes, packetBytes.size, DhcpPacket.ENCAP_L2) + assertTrue(packetType.isInstance(packet), + "Expected ${packetType.simpleName} but got ${packet.javaClass.simpleName}") + return packetType.cast(packet) +} + +private fun Context.assertHasService(manager: Class): T { + return getSystemService(manager) ?: fail("Service $manager not found") +} diff --git a/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt new file mode 100644 index 0000000000..f6fc75b5f4 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt @@ -0,0 +1,68 @@ +/* + * 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.cts + +import android.Manifest +import android.net.util.NetworkStackUtils +import android.provider.DeviceConfig +import com.android.testutils.runAsShell + +/** + * Collection of utility methods for configuring network validation. + */ +internal object NetworkValidationTestUtil { + + /** + * Clear the test network validation URLs. + */ + fun clearValidationTestUrlsDeviceConfig() { + setHttpsUrlDeviceConfig(null) + setHttpUrlDeviceConfig(null) + setUrlExpirationDeviceConfig(null) + } + + /** + * Set the test validation HTTPS URL. + * + * @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL + */ + fun setHttpsUrlDeviceConfig(url: String?) = + setConfig(NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, url) + + /** + * Set the test validation HTTP URL. + * + * @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL + */ + fun setHttpUrlDeviceConfig(url: String?) = + setConfig(NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, url) + + /** + * Set the test validation URL expiration. + * + * @see NetworkStackUtils.TEST_URL_EXPIRATION_TIME + */ + fun setUrlExpirationDeviceConfig(timestamp: Long?) = + setConfig(NetworkStackUtils.TEST_URL_EXPIRATION_TIME, timestamp?.toString()) + + private fun setConfig(configKey: String, value: String?) { + runAsShell(Manifest.permission.WRITE_DEVICE_CONFIG) { + DeviceConfig.setProperty( + DeviceConfig.NAMESPACE_CONNECTIVITY, configKey, value, false /* makeDefault */) + } + } +} \ No newline at end of file diff --git a/tests/cts/net/src/android/net/cts/NetworkWatchlistTest.java b/tests/cts/net/src/android/net/cts/NetworkWatchlistTest.java new file mode 100644 index 0000000000..81a9e30dd5 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/NetworkWatchlistTest.java @@ -0,0 +1,163 @@ +/* + * 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.cts; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assume.assumeTrue; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.platform.test.annotations.AppModeFull; +import android.os.FileUtils; +import android.os.ParcelFileDescriptor; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.compatibility.common.util.ApiLevelUtil; +import com.android.compatibility.common.util.SystemUtil; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Formatter; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class NetworkWatchlistTest { + + private static final String TEST_WATCHLIST_XML = "assets/network_watchlist_config_for_test.xml"; + private static final String TEST_EMPTY_WATCHLIST_XML = + "assets/network_watchlist_config_empty_for_test.xml"; + private static final String TMP_CONFIG_PATH = + "/data/local/tmp/network_watchlist_config_for_test.xml"; + // Generated from sha256sum network_watchlist_config_for_test.xml + private static final String TEST_WATCHLIST_CONFIG_HASH = + "B5FC4636994180D54E1E912F78178AB1D8BD2BE71D90CA9F5BBC3284E4D04ED4"; + + private ConnectivityManager mConnectivityManager; + private boolean mHasFeature; + + @Before + public void setUp() throws Exception { + mHasFeature = isAtLeastP(); + mConnectivityManager = + (ConnectivityManager) InstrumentationRegistry.getContext().getSystemService( + Context.CONNECTIVITY_SERVICE); + assumeTrue(mHasFeature); + // Set empty watchlist test config before testing + setWatchlistConfig(TEST_EMPTY_WATCHLIST_XML); + // Verify test watchlist config is not set before testing + byte[] result = mConnectivityManager.getNetworkWatchlistConfigHash(); + assertNotNull("Watchlist config does not exist", result); + assertNotEquals(TEST_WATCHLIST_CONFIG_HASH, byteArrayToHexString(result)); + } + + @After + public void tearDown() throws Exception { + if (mHasFeature) { + // Set empty watchlist test config after testing + setWatchlistConfig(TEST_EMPTY_WATCHLIST_XML); + } + } + + private void cleanup() throws IOException { + runCommand("rm " + TMP_CONFIG_PATH); + } + + private boolean isAtLeastP() throws Exception { + // TODO: replace with ApiLevelUtil.isAtLeast(Build.VERSION_CODES.P) when the P API level + // constant is defined. + return ApiLevelUtil.getCodename().compareToIgnoreCase("P") >= 0; + } + + /** + * Test if ConnectivityManager.getNetworkWatchlistConfigHash() correctly + * returns the hash of config we set. + */ + @Test + @AppModeFull(reason = "Cannot access resource file in instant app mode") + public void testGetWatchlistConfigHash() throws Exception { + // Set watchlist config file for test + setWatchlistConfig(TEST_WATCHLIST_XML); + // Test if watchlist config hash value is correct + byte[] result = mConnectivityManager.getNetworkWatchlistConfigHash(); + Assert.assertEquals(TEST_WATCHLIST_CONFIG_HASH, byteArrayToHexString(result)); + } + + private static String byteArrayToHexString(byte[] bytes) { + Formatter formatter = new Formatter(); + for (byte b : bytes) { + formatter.format("%02X", b); + } + return formatter.toString(); + } + + private void saveResourceToFile(String res, String filePath) throws IOException { + // App can't access /data/local/tmp directly, so we pipe resource to file through stdin. + ParcelFileDescriptor stdin = pipeFromStdin(filePath); + pipeResourceToFileDescriptor(res, stdin); + } + + /* Pipe stdin to a file in filePath. Returns PFD for stdin. */ + private ParcelFileDescriptor pipeFromStdin(String filePath) { + // Not all devices have symlink for /dev/stdin, so use /proc/self/fd/0 directly. + // /dev/stdin maps to /proc/self/fd/0. + return runRwCommand("cp /proc/self/fd/0 " + filePath)[1]; + } + + private void pipeResourceToFileDescriptor(String res, ParcelFileDescriptor pfd) + throws IOException { + InputStream resStream = getClass().getClassLoader().getResourceAsStream(res); + FileOutputStream fdStream = new ParcelFileDescriptor.AutoCloseOutputStream(pfd); + + FileUtils.copy(resStream, fdStream); + + try { + fdStream.close(); + } catch (IOException e) { + } + } + + private static String runCommand(String command) throws IOException { + return SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(), command); + } + + private static ParcelFileDescriptor[] runRwCommand(String command) { + return InstrumentationRegistry.getInstrumentation() + .getUiAutomation().executeShellCommandRw(command); + } + + private void setWatchlistConfig(String watchlistConfigFile) throws Exception { + cleanup(); + saveResourceToFile(watchlistConfigFile, TMP_CONFIG_PATH); + final String cmdResult = runCommand( + "cmd network_watchlist set-test-config " + TMP_CONFIG_PATH).trim(); + assertThat(cmdResult).contains("Success"); + cleanup(); + } +} diff --git a/tests/cts/net/src/android/net/cts/PacketUtils.java b/tests/cts/net/src/android/net/cts/PacketUtils.java new file mode 100644 index 0000000000..0aedecb5ad --- /dev/null +++ b/tests/cts/net/src/android/net/cts/PacketUtils.java @@ -0,0 +1,474 @@ +/* + * 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.cts; + +import static android.system.OsConstants.IPPROTO_IPV6; +import static android.system.OsConstants.IPPROTO_UDP; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.nio.ShortBuffer; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.util.Arrays; + +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +public class PacketUtils { + private static final String TAG = PacketUtils.class.getSimpleName(); + + private static final int DATA_BUFFER_LEN = 4096; + + static final int IP4_HDRLEN = 20; + static final int IP6_HDRLEN = 40; + static final int UDP_HDRLEN = 8; + static final int TCP_HDRLEN = 20; + static final int TCP_HDRLEN_WITH_TIMESTAMP_OPT = TCP_HDRLEN + 12; + + // Not defined in OsConstants + static final int IPPROTO_IPV4 = 4; + static final int IPPROTO_ESP = 50; + + // Encryption parameters + static final int AES_GCM_IV_LEN = 8; + static final int AES_CBC_IV_LEN = 16; + static final int AES_GCM_BLK_SIZE = 4; + static final int AES_CBC_BLK_SIZE = 16; + + // Encryption algorithms + static final String AES = "AES"; + static final String AES_CBC = "AES/CBC/NoPadding"; + static final String HMAC_SHA_256 = "HmacSHA256"; + + public interface Payload { + byte[] getPacketBytes(IpHeader header) throws Exception; + + void addPacketBytes(IpHeader header, ByteBuffer resultBuffer) throws Exception; + + short length(); + + int getProtocolId(); + } + + public abstract static class IpHeader { + + public final byte proto; + public final InetAddress srcAddr; + public final InetAddress dstAddr; + public final Payload payload; + + public IpHeader(int proto, InetAddress src, InetAddress dst, Payload payload) { + this.proto = (byte) proto; + this.srcAddr = src; + this.dstAddr = dst; + this.payload = payload; + } + + public abstract byte[] getPacketBytes() throws Exception; + + public abstract int getProtocolId(); + } + + public static class Ip4Header extends IpHeader { + private short checksum; + + public Ip4Header(int proto, Inet4Address src, Inet4Address dst, Payload payload) { + super(proto, src, dst, payload); + } + + public byte[] getPacketBytes() throws Exception { + ByteBuffer resultBuffer = buildHeader(); + payload.addPacketBytes(this, resultBuffer); + + return getByteArrayFromBuffer(resultBuffer); + } + + public ByteBuffer buildHeader() { + ByteBuffer bb = ByteBuffer.allocate(DATA_BUFFER_LEN); + + // Version, IHL + bb.put((byte) (0x45)); + + // DCSP, ECN + bb.put((byte) 0); + + // Total Length + bb.putShort((short) (IP4_HDRLEN + payload.length())); + + // Empty for Identification, Flags and Fragment Offset + bb.putShort((short) 0); + bb.put((byte) 0x40); + bb.put((byte) 0x00); + + // TTL + bb.put((byte) 64); + + // Protocol + bb.put(proto); + + // Header Checksum + final int ipChecksumOffset = bb.position(); + bb.putShort((short) 0); + + // Src/Dst addresses + bb.put(srcAddr.getAddress()); + bb.put(dstAddr.getAddress()); + + bb.putShort(ipChecksumOffset, calculateChecksum(bb)); + + return bb; + } + + private short calculateChecksum(ByteBuffer bb) { + int checksum = 0; + + // Calculate sum of 16-bit values, excluding checksum. IPv4 headers are always 32-bit + // aligned, so no special cases needed for unaligned values. + ShortBuffer shortBuffer = ByteBuffer.wrap(getByteArrayFromBuffer(bb)).asShortBuffer(); + while (shortBuffer.hasRemaining()) { + short val = shortBuffer.get(); + + // Wrap as needed + checksum = addAndWrapForChecksum(checksum, val); + } + + return onesComplement(checksum); + } + + public int getProtocolId() { + return IPPROTO_IPV4; + } + } + + public static class Ip6Header extends IpHeader { + public Ip6Header(int nextHeader, Inet6Address src, Inet6Address dst, Payload payload) { + super(nextHeader, src, dst, payload); + } + + public byte[] getPacketBytes() throws Exception { + ByteBuffer bb = ByteBuffer.allocate(DATA_BUFFER_LEN); + + // Version | Traffic Class (First 4 bits) + bb.put((byte) 0x60); + + // Traffic class (Last 4 bits), Flow Label + bb.put((byte) 0); + bb.put((byte) 0); + bb.put((byte) 0); + + // Payload Length + bb.putShort((short) payload.length()); + + // Next Header + bb.put(proto); + + // Hop Limit + bb.put((byte) 64); + + // Src/Dst addresses + bb.put(srcAddr.getAddress()); + bb.put(dstAddr.getAddress()); + + // Payload + payload.addPacketBytes(this, bb); + + return getByteArrayFromBuffer(bb); + } + + public int getProtocolId() { + return IPPROTO_IPV6; + } + } + + public static class BytePayload implements Payload { + public final byte[] payload; + + public BytePayload(byte[] payload) { + this.payload = payload; + } + + public int getProtocolId() { + return -1; + } + + public byte[] getPacketBytes(IpHeader header) { + ByteBuffer bb = ByteBuffer.allocate(DATA_BUFFER_LEN); + + addPacketBytes(header, bb); + return getByteArrayFromBuffer(bb); + } + + public void addPacketBytes(IpHeader header, ByteBuffer resultBuffer) { + resultBuffer.put(payload); + } + + public short length() { + return (short) payload.length; + } + } + + public static class UdpHeader implements Payload { + + public final short srcPort; + public final short dstPort; + public final Payload payload; + + public UdpHeader(int srcPort, int dstPort, Payload payload) { + this.srcPort = (short) srcPort; + this.dstPort = (short) dstPort; + this.payload = payload; + } + + public int getProtocolId() { + return IPPROTO_UDP; + } + + public short length() { + return (short) (payload.length() + 8); + } + + public byte[] getPacketBytes(IpHeader header) throws Exception { + ByteBuffer bb = ByteBuffer.allocate(DATA_BUFFER_LEN); + + addPacketBytes(header, bb); + return getByteArrayFromBuffer(bb); + } + + public void addPacketBytes(IpHeader header, ByteBuffer resultBuffer) throws Exception { + // Source, Destination port + resultBuffer.putShort(srcPort); + resultBuffer.putShort(dstPort); + + // Payload Length + resultBuffer.putShort(length()); + + // Get payload bytes for checksum + payload + ByteBuffer payloadBuffer = ByteBuffer.allocate(DATA_BUFFER_LEN); + payload.addPacketBytes(header, payloadBuffer); + byte[] payloadBytes = getByteArrayFromBuffer(payloadBuffer); + + // Checksum + resultBuffer.putShort(calculateChecksum(header, payloadBytes)); + + // Payload + resultBuffer.put(payloadBytes); + } + + private short calculateChecksum(IpHeader header, byte[] payloadBytes) throws Exception { + int newChecksum = 0; + ShortBuffer srcBuffer = ByteBuffer.wrap(header.srcAddr.getAddress()).asShortBuffer(); + ShortBuffer dstBuffer = ByteBuffer.wrap(header.dstAddr.getAddress()).asShortBuffer(); + + while (srcBuffer.hasRemaining() || dstBuffer.hasRemaining()) { + short val = srcBuffer.hasRemaining() ? srcBuffer.get() : dstBuffer.get(); + + // Wrap as needed + newChecksum = addAndWrapForChecksum(newChecksum, val); + } + + // Add pseudo-header values. Proto is 0-padded, so just use the byte. + newChecksum = addAndWrapForChecksum(newChecksum, header.proto); + newChecksum = addAndWrapForChecksum(newChecksum, length()); + newChecksum = addAndWrapForChecksum(newChecksum, srcPort); + newChecksum = addAndWrapForChecksum(newChecksum, dstPort); + newChecksum = addAndWrapForChecksum(newChecksum, length()); + + ShortBuffer payloadShortBuffer = ByteBuffer.wrap(payloadBytes).asShortBuffer(); + while (payloadShortBuffer.hasRemaining()) { + newChecksum = addAndWrapForChecksum(newChecksum, payloadShortBuffer.get()); + } + if (payload.length() % 2 != 0) { + newChecksum = + addAndWrapForChecksum( + newChecksum, (payloadBytes[payloadBytes.length - 1] << 8)); + } + + return onesComplement(newChecksum); + } + } + + public static class EspHeader implements Payload { + public final int nextHeader; + public final int spi; + public final int seqNum; + public final byte[] key; + public final byte[] payload; + + /** + * Generic constructor for ESP headers. + * + *

For Tunnel mode, payload will be a full IP header + attached payloads + * + *

For Transport mode, payload will be only the attached payloads, but with the checksum + * calculated using the pre-encryption IP header + */ + public EspHeader(int nextHeader, int spi, int seqNum, byte[] key, byte[] payload) { + this.nextHeader = nextHeader; + this.spi = spi; + this.seqNum = seqNum; + this.key = key; + this.payload = payload; + } + + public int getProtocolId() { + return IPPROTO_ESP; + } + + public short length() { + // ALWAYS uses AES-CBC, HMAC-SHA256 (128b trunc len) + return (short) + calculateEspPacketSize(payload.length, AES_CBC_IV_LEN, AES_CBC_BLK_SIZE, 128); + } + + public byte[] getPacketBytes(IpHeader header) throws Exception { + ByteBuffer bb = ByteBuffer.allocate(DATA_BUFFER_LEN); + + addPacketBytes(header, bb); + return getByteArrayFromBuffer(bb); + } + + public void addPacketBytes(IpHeader header, ByteBuffer resultBuffer) throws Exception { + ByteBuffer espPayloadBuffer = ByteBuffer.allocate(DATA_BUFFER_LEN); + espPayloadBuffer.putInt(spi); + espPayloadBuffer.putInt(seqNum); + espPayloadBuffer.put(getCiphertext(key)); + + espPayloadBuffer.put(getIcv(getByteArrayFromBuffer(espPayloadBuffer)), 0, 16); + resultBuffer.put(getByteArrayFromBuffer(espPayloadBuffer)); + } + + private byte[] getIcv(byte[] authenticatedSection) throws GeneralSecurityException { + Mac sha256HMAC = Mac.getInstance(HMAC_SHA_256); + SecretKeySpec authKey = new SecretKeySpec(key, HMAC_SHA_256); + sha256HMAC.init(authKey); + + return sha256HMAC.doFinal(authenticatedSection); + } + + /** + * Encrypts and builds ciphertext block. Includes the IV, Padding and Next-Header blocks + * + *

The ciphertext does NOT include the SPI/Sequence numbers, or the ICV. + */ + private byte[] getCiphertext(byte[] key) throws GeneralSecurityException { + int paddedLen = calculateEspEncryptedLength(payload.length, AES_CBC_BLK_SIZE); + ByteBuffer paddedPayload = ByteBuffer.allocate(paddedLen); + paddedPayload.put(payload); + + // Add padding - consecutive integers from 0x01 + int pad = 1; + while (paddedPayload.position() < paddedPayload.limit()) { + paddedPayload.put((byte) pad++); + } + + paddedPayload.position(paddedPayload.limit() - 2); + paddedPayload.put((byte) (paddedLen - 2 - payload.length)); // Pad length + paddedPayload.put((byte) nextHeader); + + // Generate Initialization Vector + byte[] iv = new byte[AES_CBC_IV_LEN]; + new SecureRandom().nextBytes(iv); + IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); + SecretKeySpec secretKeySpec = new SecretKeySpec(key, AES); + + // Encrypt payload + Cipher cipher = Cipher.getInstance(AES_CBC); + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); + byte[] encrypted = cipher.doFinal(getByteArrayFromBuffer(paddedPayload)); + + // Build ciphertext + ByteBuffer cipherText = ByteBuffer.allocate(AES_CBC_IV_LEN + encrypted.length); + cipherText.put(iv); + cipherText.put(encrypted); + + return getByteArrayFromBuffer(cipherText); + } + } + + private static int addAndWrapForChecksum(int currentChecksum, int value) { + currentChecksum += value & 0x0000ffff; + + // Wrap anything beyond the first 16 bits, and add to lower order bits + return (currentChecksum >>> 16) + (currentChecksum & 0x0000ffff); + } + + private static short onesComplement(int val) { + val = (val >>> 16) + (val & 0xffff); + + if (val == 0) return 0; + return (short) ((~val) & 0xffff); + } + + public static int calculateEspPacketSize( + int payloadLen, int cryptIvLength, int cryptBlockSize, int authTruncLen) { + final int ESP_HDRLEN = 4 + 4; // SPI + Seq# + final int ICV_LEN = authTruncLen / 8; // Auth trailer; based on truncation length + payloadLen += cryptIvLength; // Initialization Vector + + // Align to block size of encryption algorithm + payloadLen = calculateEspEncryptedLength(payloadLen, cryptBlockSize); + return payloadLen + ESP_HDRLEN + ICV_LEN; + } + + private static int calculateEspEncryptedLength(int payloadLen, int cryptBlockSize) { + payloadLen += 2; // ESP trailer + + // Align to block size of encryption algorithm + return payloadLen + calculateEspPadLen(payloadLen, cryptBlockSize); + } + + private static int calculateEspPadLen(int payloadLen, int cryptBlockSize) { + return (cryptBlockSize - (payloadLen % cryptBlockSize)) % cryptBlockSize; + } + + private static byte[] getByteArrayFromBuffer(ByteBuffer buffer) { + return Arrays.copyOfRange(buffer.array(), 0, buffer.position()); + } + + public static IpHeader getIpHeader( + int protocol, InetAddress src, InetAddress dst, Payload payload) { + if ((src instanceof Inet6Address) != (dst instanceof Inet6Address)) { + throw new IllegalArgumentException("Invalid src/dst address combination"); + } + + if (src instanceof Inet6Address) { + return new Ip6Header(protocol, (Inet6Address) src, (Inet6Address) dst, payload); + } else { + return new Ip4Header(protocol, (Inet4Address) src, (Inet4Address) dst, payload); + } + } + + /* + * Debug printing + */ + private static final char[] hexArray = "0123456789ABCDEF".toCharArray(); + + public static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(hexArray[b >>> 4]); + sb.append(hexArray[b & 0x0F]); + sb.append(' '); + } + return sb.toString(); + } +} diff --git a/tests/cts/net/src/android/net/cts/ProxyInfoTest.java b/tests/cts/net/src/android/net/cts/ProxyInfoTest.java new file mode 100644 index 0000000000..1c5624ce38 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/ProxyInfoTest.java @@ -0,0 +1,135 @@ +/* + * 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.cts; + +import static org.junit.Assert.assertArrayEquals; +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 android.net.ProxyInfo; +import android.net.Uri; +import android.os.Build; + +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; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +@RunWith(AndroidJUnit4.class) +public final class ProxyInfoTest { + private static final String TEST_HOST = "test.example.com"; + private static final int TEST_PORT = 5566; + private static final Uri TEST_URI = Uri.parse("https://test.example.com"); + // This matches android.net.ProxyInfo#LOCAL_EXCL_LIST + private static final String LOCAL_EXCL_LIST = ""; + // This matches android.net.ProxyInfo#LOCAL_HOST + private static final String LOCAL_HOST = "localhost"; + // This matches android.net.ProxyInfo#LOCAL_PORT + private static final int LOCAL_PORT = -1; + + @Rule + public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(); + + @Test + public void testConstructor() { + final ProxyInfo proxy = new ProxyInfo((ProxyInfo) null); + checkEmpty(proxy); + + assertEquals(proxy, new ProxyInfo(proxy)); + } + + @Test + public void testBuildDirectProxy() { + final ProxyInfo proxy1 = ProxyInfo.buildDirectProxy(TEST_HOST, TEST_PORT); + + assertEquals(TEST_HOST, proxy1.getHost()); + assertEquals(TEST_PORT, proxy1.getPort()); + assertArrayEquals(new String[0], proxy1.getExclusionList()); + assertEquals(Uri.EMPTY, proxy1.getPacFileUrl()); + + final List exclList = new ArrayList<>(); + exclList.add("localhost"); + exclList.add("*.exclusion.com"); + final ProxyInfo proxy2 = ProxyInfo.buildDirectProxy(TEST_HOST, TEST_PORT, exclList); + + assertEquals(TEST_HOST, proxy2.getHost()); + assertEquals(TEST_PORT, proxy2.getPort()); + assertArrayEquals(exclList.toArray(new String[0]), proxy2.getExclusionList()); + assertEquals(Uri.EMPTY, proxy2.getPacFileUrl()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testBuildPacProxy() { + final ProxyInfo proxy1 = ProxyInfo.buildPacProxy(TEST_URI); + + assertEquals(LOCAL_HOST, proxy1.getHost()); + assertEquals(LOCAL_PORT, proxy1.getPort()); + assertArrayEquals(LOCAL_EXCL_LIST.toLowerCase(Locale.ROOT).split(","), + proxy1.getExclusionList()); + assertEquals(TEST_URI, proxy1.getPacFileUrl()); + + final ProxyInfo proxy2 = ProxyInfo.buildPacProxy(TEST_URI, TEST_PORT); + + assertEquals(LOCAL_HOST, proxy2.getHost()); + assertEquals(TEST_PORT, proxy2.getPort()); + assertArrayEquals(LOCAL_EXCL_LIST.toLowerCase(Locale.ROOT).split(","), + proxy2.getExclusionList()); + assertEquals(TEST_URI, proxy2.getPacFileUrl()); + } + + @Test + public void testIsValid() { + final ProxyInfo proxy1 = ProxyInfo.buildDirectProxy(TEST_HOST, TEST_PORT); + assertTrue(proxy1.isValid()); + + // Given empty host + final ProxyInfo proxy2 = ProxyInfo.buildDirectProxy("", TEST_PORT); + assertFalse(proxy2.isValid()); + // Given invalid host + final ProxyInfo proxy3 = ProxyInfo.buildDirectProxy(".invalid.com", TEST_PORT); + assertFalse(proxy3.isValid()); + // Given invalid port. + final ProxyInfo proxy4 = ProxyInfo.buildDirectProxy(TEST_HOST, 0); + assertFalse(proxy4.isValid()); + // Given another invalid port + final ProxyInfo proxy5 = ProxyInfo.buildDirectProxy(TEST_HOST, 65536); + assertFalse(proxy5.isValid()); + // Given invalid exclusion list + final List exclList = new ArrayList<>(); + exclList.add(".invalid.com"); + exclList.add("%.test.net"); + final ProxyInfo proxy6 = ProxyInfo.buildDirectProxy(TEST_HOST, TEST_PORT, exclList); + assertFalse(proxy6.isValid()); + } + + private void checkEmpty(ProxyInfo proxy) { + assertNull(proxy.getHost()); + assertEquals(0, proxy.getPort()); + assertNull(proxy.getExclusionList()); + assertEquals(Uri.EMPTY, proxy.getPacFileUrl()); + } +} diff --git a/tests/cts/net/src/android/net/cts/ProxyTest.java b/tests/cts/net/src/android/net/cts/ProxyTest.java new file mode 100644 index 0000000000..467d12f9dc --- /dev/null +++ b/tests/cts/net/src/android/net/cts/ProxyTest.java @@ -0,0 +1,39 @@ +/* + * 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.cts; + + +import android.net.Proxy; +import android.test.AndroidTestCase; + +public class ProxyTest extends AndroidTestCase { + + public void testConstructor() { + new Proxy(); + } + + public void testAccessProperties() { + final int minValidPort = 0; + final int maxValidPort = 65535; + int defaultPort = Proxy.getDefaultPort(); + if(null == Proxy.getDefaultHost()) { + assertEquals(-1, defaultPort); + } else { + assertTrue(defaultPort >= minValidPort && defaultPort <= maxValidPort); + } + } +} diff --git a/tests/cts/net/src/android/net/cts/RssiCurveTest.java b/tests/cts/net/src/android/net/cts/RssiCurveTest.java new file mode 100644 index 0000000000..d651b7186b --- /dev/null +++ b/tests/cts/net/src/android/net/cts/RssiCurveTest.java @@ -0,0 +1,102 @@ +/* + * 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.cts; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.RssiCurve; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** CTS tests for {@link RssiCurve}. */ +@RunWith(AndroidJUnit4.class) +public class RssiCurveTest { + + @Test + public void lookupScore_constantCurve() { + // One bucket from rssi=-100 to 100 with score 10. + RssiCurve curve = new RssiCurve(-100, 200, new byte[] { 10 }); + assertThat(curve.lookupScore(-200)).isEqualTo(10); + assertThat(curve.lookupScore(-100)).isEqualTo(10); + assertThat(curve.lookupScore(0)).isEqualTo(10); + assertThat(curve.lookupScore(100)).isEqualTo(10); + assertThat(curve.lookupScore(200)).isEqualTo(10); + } + + @Test + public void lookupScore_changingCurve() { + // One bucket from -100 to 0 with score -10, and one bucket from 0 to 100 with score 10. + RssiCurve curve = new RssiCurve(-100, 100, new byte[] { -10, 10 }); + assertThat(curve.lookupScore(-200)).isEqualTo(-10); + assertThat(curve.lookupScore(-100)).isEqualTo(-10); + assertThat(curve.lookupScore(-50)).isEqualTo(-10); + assertThat(curve.lookupScore(0)).isEqualTo(10); + assertThat(curve.lookupScore(50)).isEqualTo(10); + assertThat(curve.lookupScore(100)).isEqualTo(10); + assertThat(curve.lookupScore(200)).isEqualTo(10); + } + + @Test + public void lookupScore_linearCurve() { + // Curve starting at -110, with 15 buckets of width 10 whose scores increases by 10 with + // each bucket. The current active network gets a boost of 15 to its RSSI. + RssiCurve curve = new RssiCurve( + -110, + 10, + new byte[] { -20, -10, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120 }, + 15); + + assertThat(curve.lookupScore(-120)).isEqualTo(-20); + assertThat(curve.lookupScore(-120, false)).isEqualTo(-20); + assertThat(curve.lookupScore(-120, true)).isEqualTo(-20); + + assertThat(curve.lookupScore(-111)).isEqualTo(-20); + assertThat(curve.lookupScore(-111, false)).isEqualTo(-20); + assertThat(curve.lookupScore(-111, true)).isEqualTo(-10); + + assertThat(curve.lookupScore(-110)).isEqualTo(-20); + assertThat(curve.lookupScore(-110, false)).isEqualTo(-20); + assertThat(curve.lookupScore(-110, true)).isEqualTo(-10); + + assertThat(curve.lookupScore(-105)).isEqualTo(-20); + assertThat(curve.lookupScore(-105, false)).isEqualTo(-20); + assertThat(curve.lookupScore(-105, true)).isEqualTo(0); + + assertThat(curve.lookupScore(-100)).isEqualTo(-10); + assertThat(curve.lookupScore(-100, false)).isEqualTo(-10); + assertThat(curve.lookupScore(-100, true)).isEqualTo(0); + + assertThat(curve.lookupScore(-50)).isEqualTo(40); + assertThat(curve.lookupScore(-50, false)).isEqualTo(40); + assertThat(curve.lookupScore(-50, true)).isEqualTo(50); + + assertThat(curve.lookupScore(0)).isEqualTo(90); + assertThat(curve.lookupScore(0, false)).isEqualTo(90); + assertThat(curve.lookupScore(0, true)).isEqualTo(100); + + assertThat(curve.lookupScore(30)).isEqualTo(120); + assertThat(curve.lookupScore(30, false)).isEqualTo(120); + assertThat(curve.lookupScore(30, true)).isEqualTo(120); + + assertThat(curve.lookupScore(40)).isEqualTo(120); + assertThat(curve.lookupScore(40, false)).isEqualTo(120); + assertThat(curve.lookupScore(40, true)).isEqualTo(120); + } +} diff --git a/tests/cts/net/src/android/net/cts/SSLCertificateSocketFactoryTest.java b/tests/cts/net/src/android/net/cts/SSLCertificateSocketFactoryTest.java new file mode 100644 index 0000000000..cbe54f8036 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/SSLCertificateSocketFactoryTest.java @@ -0,0 +1,348 @@ +/* + * Copyright (C) 2008 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.cts; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.net.SSLCertificateSocketFactory; +import android.platform.test.annotations.AppModeFull; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import libcore.javax.net.ssl.SSLConfigurationAsserts; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class SSLCertificateSocketFactoryTest { + // TEST_HOST should point to a web server with a valid TLS certificate. + private static final String TEST_HOST = "www.google.com"; + private static final int HTTPS_PORT = 443; + private HostnameVerifier mDefaultVerifier; + private SSLCertificateSocketFactory mSocketFactory; + private InetAddress mLocalAddress; + // InetAddress obtained by resolving TEST_HOST. + private InetAddress mTestHostAddress; + // SocketAddress combining mTestHostAddress and HTTPS_PORT. + private List mTestSocketAddresses; + + @Before + public void setUp() { + // Expected state before each test method is that + // HttpsURLConnection.getDefaultHostnameVerifier() will return the system default. + mDefaultVerifier = HttpsURLConnection.getDefaultHostnameVerifier(); + mSocketFactory = (SSLCertificateSocketFactory) + SSLCertificateSocketFactory.getDefault(1000 /* handshakeTimeoutMillis */); + assertNotNull(mSocketFactory); + InetAddress[] addresses; + try { + addresses = InetAddress.getAllByName(TEST_HOST); + mTestHostAddress = addresses[0]; + } catch (UnknownHostException uhe) { + throw new AssertionError( + "Unable to test SSLCertificateSocketFactory: cannot resolve " + TEST_HOST, uhe); + } + + mTestSocketAddresses = Arrays.stream(addresses) + .map(addr -> new InetSocketAddress(addr, HTTPS_PORT)) + .collect(Collectors.toList()); + + // Find the local IP address which will be used to connect to TEST_HOST. + try { + Socket testSocket = new Socket(TEST_HOST, HTTPS_PORT); + mLocalAddress = testSocket.getLocalAddress(); + testSocket.close(); + } catch (IOException ioe) { + throw new AssertionError("" + + "Unable to test SSLCertificateSocketFactory: cannot connect to " + + TEST_HOST, ioe); + } + } + + // Restore the system default hostname verifier after each test. + @After + public void restoreDefaultHostnameVerifier() { + HttpsURLConnection.setDefaultHostnameVerifier(mDefaultVerifier); + } + + @Test + public void testDefaultConfiguration() throws Exception { + SSLConfigurationAsserts.assertSSLSocketFactoryDefaultConfiguration(mSocketFactory); + } + + @Test + public void testAccessProperties() { + mSocketFactory.getSupportedCipherSuites(); + mSocketFactory.getDefaultCipherSuites(); + } + + /** + * Tests the {@code createSocket()} cases which are expected to fail with {@code IOException}. + */ + @Test + @AppModeFull(reason = "Socket cannot bind in instant app mode") + public void createSocket_io_error_expected() { + // Connect to the localhost HTTPS port. Should result in connection refused IOException + // because no service should be listening on that port. + InetAddress localhostAddress = InetAddress.getLoopbackAddress(); + try { + mSocketFactory.createSocket(localhostAddress, HTTPS_PORT); + fail(); + } catch (IOException e) { + // expected + } + + // Same, but also binding to a local address. + try { + mSocketFactory.createSocket(localhostAddress, HTTPS_PORT, localhostAddress, 0); + fail(); + } catch (IOException e) { + // expected + } + + // Same, wrapping an existing plain socket which is in an unconnected state. + try { + Socket socket = new Socket(); + mSocketFactory.createSocket(socket, "localhost", HTTPS_PORT, true); + fail(); + } catch (IOException e) { + // expected + } + } + + /** + * Tests hostname verification for + * {@link SSLCertificateSocketFactory#createSocket(String, int)}. + * + *

This method should return a socket which is fully connected (i.e. TLS handshake complete) + * and whose peer TLS certificate has been verified to have the correct hostname. + * + *

{@link SSLCertificateSocketFactory} is documented to verify hostnames using + * the {@link HostnameVerifier} returned by + * {@link HttpsURLConnection#getDefaultHostnameVerifier}, so this test connects twice, + * once with the system default {@link HostnameVerifier} which is expected to succeed, + * and once after installing a {@link NegativeHostnameVerifier} which will cause + * {@link SSLCertificateSocketFactory#verifyHostname} to throw a + * {@link SSLPeerUnverifiedException}. + * + *

These tests only test the hostname verification logic in SSLCertificateSocketFactory, + * other TLS failure modes and the default HostnameVerifier are tested elsewhere, see + * {@link com.squareup.okhttp.internal.tls.HostnameVerifierTest} and + * https://android.googlesource.com/platform/external/boringssl/+/refs/heads/master/src/ssl/test + * + *

Tests the following behaviour:- + *

    + *
  • TEST_SERVER is available and has a valid TLS certificate + *
  • {@code createSocket()} verifies the remote hostname is correct using + * {@link HttpsURLConnection#getDefaultHostnameVerifier} + *
  • {@link SSLPeerUnverifiedException} is thrown when the remote hostname is invalid + *
+ * + *

See also http://b/2807618. + */ + @Test + public void createSocket_simple_with_hostname_verification() throws Exception { + Socket socket = mSocketFactory.createSocket(TEST_HOST, HTTPS_PORT); + assertConnectedSocket(socket); + socket.close(); + + HttpsURLConnection.setDefaultHostnameVerifier(new NegativeHostnameVerifier()); + try { + mSocketFactory.createSocket(TEST_HOST, HTTPS_PORT); + fail(); + } catch (SSLPeerUnverifiedException expected) { + // expected + } + } + + /** + * Tests hostname verification for + * {@link SSLCertificateSocketFactory#createSocket(Socket, String, int, boolean)}. + * + *

This method should return a socket which is fully connected (i.e. TLS handshake complete) + * and whose peer TLS certificate has been verified to have the correct hostname. + * + *

The TLS socket returned is wrapped around the plain socket passed into + * {@code createSocket()}. + * + *

See {@link #createSocket_simple_with_hostname_verification()} for test methodology. + */ + @Test + public void createSocket_wrapped_with_hostname_verification() throws Exception { + Socket underlying = new Socket(TEST_HOST, HTTPS_PORT); + Socket socket = mSocketFactory.createSocket(underlying, TEST_HOST, HTTPS_PORT, true); + assertConnectedSocket(socket); + socket.close(); + + HttpsURLConnection.setDefaultHostnameVerifier(new NegativeHostnameVerifier()); + try { + underlying = new Socket(TEST_HOST, HTTPS_PORT); + mSocketFactory.createSocket(underlying, TEST_HOST, HTTPS_PORT, true); + fail(); + } catch (SSLPeerUnverifiedException expected) { + // expected + } + } + + /** + * Tests hostname verification for + * {@link SSLCertificateSocketFactory#createSocket(String, int, InetAddress, int)}. + * + *

This method should return a socket which is fully connected (i.e. TLS handshake complete) + * and whose peer TLS certificate has been verified to have the correct hostname. + * + *

The TLS socket returned is also bound to the local address determined in {@link #setUp} to + * be used for connections to TEST_HOST, and a wildcard port. + * + *

See {@link #createSocket_simple_with_hostname_verification()} for test methodology. + */ + @Test + @AppModeFull(reason = "Socket cannot bind in instant app mode") + public void createSocket_bound_with_hostname_verification() throws Exception { + Socket socket = mSocketFactory.createSocket(TEST_HOST, HTTPS_PORT, mLocalAddress, 0); + assertConnectedSocket(socket); + socket.close(); + + HttpsURLConnection.setDefaultHostnameVerifier(new NegativeHostnameVerifier()); + try { + mSocketFactory.createSocket(TEST_HOST, HTTPS_PORT, mLocalAddress, 0); + fail(); + } catch (SSLPeerUnverifiedException expected) { + // expected + } + } + + /** + * Tests hostname verification for + * {@link SSLCertificateSocketFactory#createSocket(InetAddress, int)}. + * + *

This method should return a socket which the documentation describes as "unconnected", + * which actually means that the socket is fully connected at the TCP layer but TLS handshaking + * and hostname verification have not yet taken place. + * + *

Behaviour is tested by installing a {@link NegativeHostnameVerifier} and by calling + * {@link #assertConnectedSocket} to ensure TLS handshaking but no hostname verification takes + * place. Next, {@link SSLCertificateSocketFactory#verifyHostname} is called to ensure + * that hostname verification is using the {@link HostnameVerifier} returned by + * {@link HttpsURLConnection#getDefaultHostnameVerifier} as documented. + * + *

Tests the following behaviour:- + *

    + *
  • TEST_SERVER is available and has a valid TLS certificate + *
  • {@code createSocket()} does not verify the remote hostname + *
  • Calling {@link SSLCertificateSocketFactory#verifyHostname} on the returned socket + * throws {@link SSLPeerUnverifiedException} if the remote hostname is invalid + *
+ */ + @Test + public void createSocket_simple_no_hostname_verification() throws Exception{ + HttpsURLConnection.setDefaultHostnameVerifier(new NegativeHostnameVerifier()); + Socket socket = mSocketFactory.createSocket(mTestHostAddress, HTTPS_PORT); + // Need to provide the expected hostname here or the TLS handshake will + // be unable to supply SNI to the remote host. + mSocketFactory.setHostname(socket, TEST_HOST); + assertConnectedSocket(socket); + try { + SSLCertificateSocketFactory.verifyHostname(socket, TEST_HOST); + fail(); + } catch (SSLPeerUnverifiedException expected) { + // expected + } + HttpsURLConnection.setDefaultHostnameVerifier(mDefaultVerifier); + SSLCertificateSocketFactory.verifyHostname(socket, TEST_HOST); + socket.close(); + } + + /** + * Tests hostname verification for + * {@link SSLCertificateSocketFactory#createSocket(InetAddress, int, InetAddress, int)}. + * + *

This method should return a socket which the documentation describes as "unconnected", + * which actually means that the socket is fully connected at the TCP layer but TLS handshaking + * and hostname verification have not yet taken place. + * + *

The TLS socket returned is also bound to the local address determined in {@link #setUp} to + * be used for connections to TEST_HOST, and a wildcard port. + * + *

See {@link #createSocket_simple_no_hostname_verification()} for test methodology. + */ + @Test + @AppModeFull(reason = "Socket cannot bind in instant app mode") + public void createSocket_bound_no_hostname_verification() throws Exception{ + HttpsURLConnection.setDefaultHostnameVerifier(new NegativeHostnameVerifier()); + Socket socket = + mSocketFactory.createSocket(mTestHostAddress, HTTPS_PORT, mLocalAddress, 0); + // Need to provide the expected hostname here or the TLS handshake will + // be unable to supply SNI to the peer. + mSocketFactory.setHostname(socket, TEST_HOST); + assertConnectedSocket(socket); + try { + SSLCertificateSocketFactory.verifyHostname(socket, TEST_HOST); + fail(); + } catch (SSLPeerUnverifiedException expected) { + // expected + } + HttpsURLConnection.setDefaultHostnameVerifier(mDefaultVerifier); + SSLCertificateSocketFactory.verifyHostname(socket, TEST_HOST); + socket.close(); + } + + /** + * Asserts a socket is fully connected to the expected peer. + * + *

For the variants of createSocket which verify the remote hostname, + * {@code socket} should already be fully connected. + * + *

For the non-verifying variants, retrieving the input stream will trigger a TLS handshake + * and so may throw an exception, for example if the peer's certificate is invalid. + * + *

Does no hostname verification. + */ + private void assertConnectedSocket(Socket socket) throws Exception { + assertNotNull(socket); + assertTrue(socket.isConnected()); + assertNotNull(socket.getInputStream()); + assertNotNull(socket.getOutputStream()); + assertTrue(mTestSocketAddresses.contains(socket.getRemoteSocketAddress())); + } + + /** + * A HostnameVerifier which always returns false to simulate a server returning a + * certificate which does not match the expected hostname. + */ + private static class NegativeHostnameVerifier implements HostnameVerifier { + @Override + public boolean verify(String hostname, SSLSession sslSession) { + return false; + } + } +} diff --git a/tests/cts/net/src/android/net/cts/TheaterModeTest.java b/tests/cts/net/src/android/net/cts/TheaterModeTest.java new file mode 100644 index 0000000000..d1ddeaa375 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/TheaterModeTest.java @@ -0,0 +1,84 @@ +/* + * 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.cts; + +import android.content.ContentResolver; +import android.content.Context; +import android.platform.test.annotations.AppModeFull; +import android.provider.Settings; +import android.test.AndroidTestCase; +import android.util.Log; + +public class TheaterModeTest extends AndroidTestCase { + private static final String TAG = "TheaterModeTest"; + private static final String FEATURE_BLUETOOTH = "android.hardware.bluetooth"; + private static final String FEATURE_WIFI = "android.hardware.wifi"; + private static final int TIMEOUT_MS = 10 * 1000; + private boolean mHasFeature; + private Context mContext; + private ContentResolver resolver; + + public void setup() { + mContext= getContext(); + resolver = mContext.getContentResolver(); + mHasFeature = (mContext.getPackageManager().hasSystemFeature(FEATURE_BLUETOOTH) + || mContext.getPackageManager().hasSystemFeature(FEATURE_WIFI)); + } + + @AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps") + public void testTheaterMode() { + setup(); + if (!mHasFeature) { + Log.i(TAG, "The device doesn't support network bluetooth or wifi feature"); + return; + } + + for (int testCount = 0; testCount < 2; testCount++) { + if (!doOneTest()) { + fail("Theater mode failed to change in " + TIMEOUT_MS + "msec"); + return; + } + } + } + + private boolean doOneTest() { + boolean theaterModeOn = isTheaterModeOn(); + + setTheaterModeOn(!theaterModeOn); + try { + Thread.sleep(TIMEOUT_MS); + } catch (InterruptedException e) { + Log.e(TAG, "Sleep time interrupted.", e); + } + + if (theaterModeOn == isTheaterModeOn()) { + return false; + } + return true; + } + + private void setTheaterModeOn(boolean enabling) { + // Change the system setting for theater mode + Settings.Global.putInt(resolver, Settings.Global.THEATER_MODE_ON, enabling ? 1 : 0); + } + + private boolean isTheaterModeOn() { + // Read the system setting for theater mode + return Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.THEATER_MODE_ON, 0) != 0; + } +} diff --git a/tests/cts/net/src/android/net/cts/TrafficStatsTest.java b/tests/cts/net/src/android/net/cts/TrafficStatsTest.java new file mode 100755 index 0000000000..37bdd44fbf --- /dev/null +++ b/tests/cts/net/src/android/net/cts/TrafficStatsTest.java @@ -0,0 +1,279 @@ +/* + * 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.cts; + +import android.net.NetworkStats; +import android.net.TrafficStats; +import android.os.Process; +import android.platform.test.annotations.AppModeFull; +import android.test.AndroidTestCase; +import android.util.Log; +import android.util.Range; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class TrafficStatsTest extends AndroidTestCase { + private static final String LOG_TAG = "TrafficStatsTest"; + + /** Verify the given value is in range [lower, upper] */ + private void assertInRange(String tag, long value, long lower, long upper) { + final Range range = new Range(lower, upper); + assertTrue(tag + ": " + value + " is not within range [" + lower + ", " + upper + "]", + range.contains(value)); + } + + public void testValidMobileStats() { + // We can't assume a mobile network is even present in this test, so + // we simply assert that a valid value is returned. + + assertTrue(TrafficStats.getMobileTxPackets() >= 0); + assertTrue(TrafficStats.getMobileRxPackets() >= 0); + assertTrue(TrafficStats.getMobileTxBytes() >= 0); + assertTrue(TrafficStats.getMobileRxBytes() >= 0); + } + + public void testValidTotalStats() { + assertTrue(TrafficStats.getTotalTxPackets() >= 0); + assertTrue(TrafficStats.getTotalRxPackets() >= 0); + assertTrue(TrafficStats.getTotalTxBytes() >= 0); + assertTrue(TrafficStats.getTotalRxBytes() >= 0); + } + + public void testValidPacketStats() { + assertTrue(TrafficStats.getTxPackets("lo") >= 0); + assertTrue(TrafficStats.getRxPackets("lo") >= 0); + } + + public void testThreadStatsTag() throws Exception { + TrafficStats.setThreadStatsTag(0xf00d); + assertTrue("Tag didn't stick", TrafficStats.getThreadStatsTag() == 0xf00d); + + final CountDownLatch latch = new CountDownLatch(1); + + new Thread("TrafficStatsTest.testThreadStatsTag") { + @Override + public void run() { + assertTrue("Tag leaked", TrafficStats.getThreadStatsTag() != 0xf00d); + TrafficStats.setThreadStatsTag(0xcafe); + assertTrue("Tag didn't stick", TrafficStats.getThreadStatsTag() == 0xcafe); + latch.countDown(); + } + }.start(); + + latch.await(5, TimeUnit.SECONDS); + assertTrue("Tag lost", TrafficStats.getThreadStatsTag() == 0xf00d); + + TrafficStats.clearThreadStatsTag(); + assertTrue("Tag not cleared", TrafficStats.getThreadStatsTag() != 0xf00d); + } + + long tcpPacketToIpBytes(long packetCount, long bytes) { + // ip header + tcp header + data. + // Tcp header is mostly 32. Syn has different tcp options -> 40. Don't care. + return packetCount * (20 + 32 + bytes); + } + + @AppModeFull(reason = "Socket cannot bind in instant app mode") + public void testTrafficStatsForLocalhost() throws IOException { + final long mobileTxPacketsBefore = TrafficStats.getMobileTxPackets(); + final long mobileRxPacketsBefore = TrafficStats.getMobileRxPackets(); + final long mobileTxBytesBefore = TrafficStats.getMobileTxBytes(); + final long mobileRxBytesBefore = TrafficStats.getMobileRxBytes(); + final long totalTxPacketsBefore = TrafficStats.getTotalTxPackets(); + final long totalRxPacketsBefore = TrafficStats.getTotalRxPackets(); + final long totalTxBytesBefore = TrafficStats.getTotalTxBytes(); + final long totalRxBytesBefore = TrafficStats.getTotalRxBytes(); + final long uidTxBytesBefore = TrafficStats.getUidTxBytes(Process.myUid()); + final long uidRxBytesBefore = TrafficStats.getUidRxBytes(Process.myUid()); + final long uidTxPacketsBefore = TrafficStats.getUidTxPackets(Process.myUid()); + final long uidRxPacketsBefore = TrafficStats.getUidRxPackets(Process.myUid()); + final long ifaceTxPacketsBefore = TrafficStats.getTxPackets("lo"); + final long ifaceRxPacketsBefore = TrafficStats.getRxPackets("lo"); + + // Transfer 1MB of data across an explicitly localhost socket. + final int byteCount = 1024; + final int packetCount = 1024; + + TrafficStats.startDataProfiling(null); + final ServerSocket server = new ServerSocket(0); + new Thread("TrafficStatsTest.testTrafficStatsForLocalhost") { + @Override + public void run() { + try { + final Socket socket = new Socket("localhost", server.getLocalPort()); + // Make sure that each write()+flush() turns into a packet: + // disable Nagle. + socket.setTcpNoDelay(true); + final OutputStream out = socket.getOutputStream(); + final byte[] buf = new byte[byteCount]; + TrafficStats.setThreadStatsTag(0x42); + TrafficStats.tagSocket(socket); + for (int i = 0; i < packetCount; i++) { + out.write(buf); + out.flush(); + try { + // Bug: 10668088, Even with Nagle disabled, and flushing the 1024 bytes + // the kernel still regroups data into a larger packet. + Thread.sleep(5); + } catch (InterruptedException e) { + } + } + out.close(); + socket.close(); + } catch (IOException e) { + Log.i(LOG_TAG, "Badness during writes to socket: " + e); + } + } + }.start(); + + int read = 0; + try { + final Socket socket = server.accept(); + socket.setTcpNoDelay(true); + TrafficStats.setThreadStatsTag(0x43); + TrafficStats.tagSocket(socket); + final InputStream in = socket.getInputStream(); + final byte[] buf = new byte[byteCount]; + while (read < byteCount * packetCount) { + int n = in.read(buf); + assertTrue("Unexpected EOF", n > 0); + read += n; + } + } finally { + server.close(); + } + assertTrue("Not all data read back", read >= byteCount * packetCount); + + // It's too fast to call getUidTxBytes function. + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + } + final NetworkStats testStats = TrafficStats.stopDataProfiling(null); + + final long mobileTxPacketsAfter = TrafficStats.getMobileTxPackets(); + final long mobileRxPacketsAfter = TrafficStats.getMobileRxPackets(); + final long mobileTxBytesAfter = TrafficStats.getMobileTxBytes(); + final long mobileRxBytesAfter = TrafficStats.getMobileRxBytes(); + final long totalTxPacketsAfter = TrafficStats.getTotalTxPackets(); + final long totalRxPacketsAfter = TrafficStats.getTotalRxPackets(); + final long totalTxBytesAfter = TrafficStats.getTotalTxBytes(); + final long totalRxBytesAfter = TrafficStats.getTotalRxBytes(); + final long uidTxBytesAfter = TrafficStats.getUidTxBytes(Process.myUid()); + final long uidRxBytesAfter = TrafficStats.getUidRxBytes(Process.myUid()); + final long uidTxPacketsAfter = TrafficStats.getUidTxPackets(Process.myUid()); + final long uidRxPacketsAfter = TrafficStats.getUidRxPackets(Process.myUid()); + final long uidTxDeltaBytes = uidTxBytesAfter - uidTxBytesBefore; + final long uidTxDeltaPackets = uidTxPacketsAfter - uidTxPacketsBefore; + final long uidRxDeltaBytes = uidRxBytesAfter - uidRxBytesBefore; + final long uidRxDeltaPackets = uidRxPacketsAfter - uidRxPacketsBefore; + final long ifaceTxPacketsAfter = TrafficStats.getTxPackets("lo"); + final long ifaceRxPacketsAfter = TrafficStats.getRxPackets("lo"); + final long ifaceTxDeltaPackets = ifaceTxPacketsAfter - ifaceTxPacketsBefore; + final long ifaceRxDeltaPackets = ifaceRxPacketsAfter - ifaceRxPacketsBefore; + + // Localhost traffic *does* count against per-UID stats. + /* + * Calculations: + * - bytes + * bytes is approx: packets * data + packets * acks; + * but sometimes there are less acks than packets, so we set a lower + * limit of 1 ack. + * - setup/teardown + * + 7 approx.: syn, syn-ack, ack, fin-ack, ack, fin-ack, ack; + * but sometimes the last find-acks just vanish, so we set a lower limit of +5. + */ + final int maxExpectedExtraPackets = 7; + final int minExpectedExtraPackets = 5; + + // Some other tests don't cleanup connections correctly. + // They have the same UID, so we discount their lingering traffic + // which happens only on non-localhost, such as TCP FIN retranmission packets + final long deltaTxOtherPackets = (totalTxPacketsAfter - totalTxPacketsBefore) + - uidTxDeltaPackets; + final long deltaRxOtherPackets = (totalRxPacketsAfter - totalRxPacketsBefore) + - uidRxDeltaPackets; + if (deltaTxOtherPackets > 0 || deltaRxOtherPackets > 0) { + Log.i(LOG_TAG, "lingering traffic data: " + deltaTxOtherPackets + "/" + + deltaRxOtherPackets); + } + + // Check that the per-uid stats obtained from data profiling contain the expected values. + // The data profiling snapshot is generated from the readNetworkStatsDetail() method in + // networkStatsService, so it's possible to verify that the detailed stats for a given + // uid are correct. + final NetworkStats.Entry entry = testStats.getTotal(null, Process.myUid()); + final long pktBytes = tcpPacketToIpBytes(packetCount, byteCount); + final long pktWithNoDataBytes = tcpPacketToIpBytes(packetCount, 0); + final long minExpExtraPktBytes = tcpPacketToIpBytes(minExpectedExtraPackets, 0); + final long maxExpExtraPktBytes = tcpPacketToIpBytes(maxExpectedExtraPackets, 0); + final long deltaTxOtherPktBytes = tcpPacketToIpBytes(deltaTxOtherPackets, 0); + final long deltaRxOtherPktBytes = tcpPacketToIpBytes(deltaRxOtherPackets, 0); + assertInRange("txPackets detail", entry.txPackets, packetCount + minExpectedExtraPackets, + uidTxDeltaPackets); + assertInRange("rxPackets detail", entry.rxPackets, packetCount + minExpectedExtraPackets, + uidRxDeltaPackets); + assertInRange("txBytes detail", entry.txBytes, pktBytes + minExpExtraPktBytes, + uidTxDeltaBytes); + assertInRange("rxBytes detail", entry.rxBytes, pktBytes + minExpExtraPktBytes, + uidRxDeltaBytes); + assertInRange("uidtxp", uidTxDeltaPackets, packetCount + minExpectedExtraPackets, + packetCount + packetCount + maxExpectedExtraPackets + deltaTxOtherPackets); + assertInRange("uidrxp", uidRxDeltaPackets, packetCount + minExpectedExtraPackets, + packetCount + packetCount + maxExpectedExtraPackets + deltaRxOtherPackets); + assertInRange("uidtxb", uidTxDeltaBytes, pktBytes + minExpExtraPktBytes, + pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes + deltaTxOtherPktBytes); + assertInRange("uidrxb", uidRxDeltaBytes, pktBytes + minExpExtraPktBytes, + pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes + deltaRxOtherPktBytes); + assertInRange("iftxp", ifaceTxDeltaPackets, packetCount + minExpectedExtraPackets, + packetCount + packetCount + maxExpectedExtraPackets + deltaTxOtherPackets); + assertInRange("ifrxp", ifaceRxDeltaPackets, packetCount + minExpectedExtraPackets, + packetCount + packetCount + maxExpectedExtraPackets + deltaRxOtherPackets); + + // Localhost traffic *does* count against total stats. + // Check the total stats increased after test data transfer over localhost has been made. + assertTrue("ttxp: " + totalTxPacketsBefore + " -> " + totalTxPacketsAfter, + totalTxPacketsAfter >= totalTxPacketsBefore + uidTxDeltaPackets); + assertTrue("trxp: " + totalRxPacketsBefore + " -> " + totalRxPacketsAfter, + totalRxPacketsAfter >= totalRxPacketsBefore + uidRxDeltaPackets); + assertTrue("ttxb: " + totalTxBytesBefore + " -> " + totalTxBytesAfter, + totalTxBytesAfter >= totalTxBytesBefore + uidTxDeltaBytes); + assertTrue("trxb: " + totalRxBytesBefore + " -> " + totalRxBytesAfter, + totalRxBytesAfter >= totalRxBytesBefore + uidRxDeltaBytes); + assertTrue("iftxp: " + ifaceTxPacketsBefore + " -> " + ifaceTxPacketsAfter, + totalTxPacketsAfter >= totalTxPacketsBefore + ifaceTxDeltaPackets); + assertTrue("ifrxp: " + ifaceRxPacketsBefore + " -> " + ifaceRxPacketsAfter, + totalRxPacketsAfter >= totalRxPacketsBefore + ifaceRxDeltaPackets); + + // Localhost traffic should *not* count against mobile stats, + // There might be some other traffic, but nowhere near 1MB. + assertInRange("mtxp", mobileTxPacketsAfter, mobileTxPacketsBefore, + mobileTxPacketsBefore + 500); + assertInRange("mrxp", mobileRxPacketsAfter, mobileRxPacketsBefore, + mobileRxPacketsBefore + 500); + assertInRange("mtxb", mobileTxBytesAfter, mobileTxBytesBefore, + mobileTxBytesBefore + 200000); + assertInRange("mrxb", mobileRxBytesAfter, mobileRxBytesBefore, + mobileRxBytesBefore + 200000); + } +} diff --git a/tests/cts/net/src/android/net/cts/TunUtils.java b/tests/cts/net/src/android/net/cts/TunUtils.java new file mode 100644 index 0000000000..adaba9d398 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/TunUtils.java @@ -0,0 +1,254 @@ +/* + * 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.cts; + +import static android.net.cts.PacketUtils.IP4_HDRLEN; +import static android.net.cts.PacketUtils.IP6_HDRLEN; +import static android.net.cts.PacketUtils.IPPROTO_ESP; +import static android.net.cts.PacketUtils.UDP_HDRLEN; +import static android.system.OsConstants.IPPROTO_UDP; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import android.os.ParcelFileDescriptor; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; + +public class TunUtils { + private static final String TAG = TunUtils.class.getSimpleName(); + + protected static final int IP4_ADDR_OFFSET = 12; + protected static final int IP4_ADDR_LEN = 4; + protected static final int IP6_ADDR_OFFSET = 8; + protected static final int IP6_ADDR_LEN = 16; + protected static final int IP4_PROTO_OFFSET = 9; + protected static final int IP6_PROTO_OFFSET = 6; + + private static final int DATA_BUFFER_LEN = 4096; + private static final int TIMEOUT = 1000; + + private final List mPackets = new ArrayList<>(); + private final ParcelFileDescriptor mTunFd; + private final Thread mReaderThread; + + public TunUtils(ParcelFileDescriptor tunFd) { + mTunFd = tunFd; + + // Start background reader thread + mReaderThread = + new Thread( + () -> { + try { + // Loop will exit and thread will quit when tunFd is closed. + // Receiving either EOF or an exception will exit this reader loop. + // FileInputStream in uninterruptable, so there's no good way to + // ensure that this thread shuts down except upon FD closure. + while (true) { + byte[] intercepted = receiveFromTun(); + if (intercepted == null) { + // Exit once we've hit EOF + return; + } else if (intercepted.length > 0) { + // Only save packet if we've received any bytes. + synchronized (mPackets) { + mPackets.add(intercepted); + mPackets.notifyAll(); + } + } + } + } catch (IOException ignored) { + // Simply exit this reader thread + return; + } + }); + mReaderThread.start(); + } + + private byte[] receiveFromTun() throws IOException { + FileInputStream in = new FileInputStream(mTunFd.getFileDescriptor()); + byte[] inBytes = new byte[DATA_BUFFER_LEN]; + int bytesRead = in.read(inBytes); + + if (bytesRead < 0) { + return null; // return null for EOF + } else if (bytesRead >= DATA_BUFFER_LEN) { + throw new IllegalStateException("Too big packet. Fragmentation unsupported"); + } + return Arrays.copyOf(inBytes, bytesRead); + } + + private byte[] getFirstMatchingPacket(Predicate verifier, int startIndex) { + synchronized (mPackets) { + for (int i = startIndex; i < mPackets.size(); i++) { + byte[] pkt = mPackets.get(i); + if (verifier.test(pkt)) { + return pkt; + } + } + } + return null; + } + + protected byte[] awaitPacket(Predicate verifier) throws Exception { + long endTime = System.currentTimeMillis() + TIMEOUT; + int startIndex = 0; + + synchronized (mPackets) { + while (System.currentTimeMillis() < endTime) { + final byte[] pkt = getFirstMatchingPacket(verifier, startIndex); + if (pkt != null) { + return pkt; // We've found the packet we're looking for. + } + + startIndex = mPackets.size(); + + // Try to prevent waiting too long. If waitTimeout <= 0, we've already hit timeout + long waitTimeout = endTime - System.currentTimeMillis(); + if (waitTimeout > 0) { + mPackets.wait(waitTimeout); + } + } + } + + fail("No packet found matching verifier"); + throw new IllegalStateException("Impossible condition; should have thrown in fail()"); + } + + public byte[] awaitEspPacketNoPlaintext( + int spi, byte[] plaintext, boolean useEncap, int expectedPacketSize) throws Exception { + final byte[] espPkt = awaitPacket( + (pkt) -> isEspFailIfSpecifiedPlaintextFound(pkt, spi, useEncap, plaintext)); + + // Validate packet size + assertEquals(expectedPacketSize, espPkt.length); + + return espPkt; // We've found the packet we're looking for. + } + + private static boolean isSpiEqual(byte[] pkt, int espOffset, int spi) { + // Check SPI byte by byte. + return pkt[espOffset] == (byte) ((spi >>> 24) & 0xff) + && pkt[espOffset + 1] == (byte) ((spi >>> 16) & 0xff) + && pkt[espOffset + 2] == (byte) ((spi >>> 8) & 0xff) + && pkt[espOffset + 3] == (byte) (spi & 0xff); + } + + /** + * Variant of isEsp that also fails the test if the provided plaintext is found + * + * @param pkt the packet bytes to verify + * @param spi the expected SPI to look for + * @param encap whether encap was enabled, and the packet has a UDP header + * @param plaintext the plaintext packet before outbound encryption, which MUST not appear in + * the provided packet. + */ + private static boolean isEspFailIfSpecifiedPlaintextFound( + byte[] pkt, int spi, boolean encap, byte[] plaintext) { + if (Collections.indexOfSubList(Arrays.asList(pkt), Arrays.asList(plaintext)) != -1) { + fail("Banned plaintext packet found"); + } + + return isEsp(pkt, spi, encap); + } + + private static boolean isEsp(byte[] pkt, int spi, boolean encap) { + if (isIpv6(pkt)) { + // IPv6 UDP encap not supported by kernels; assume non-encap. + return pkt[IP6_PROTO_OFFSET] == IPPROTO_ESP && isSpiEqual(pkt, IP6_HDRLEN, spi); + } else { + // Use default IPv4 header length (assuming no options) + if (encap) { + return pkt[IP4_PROTO_OFFSET] == IPPROTO_UDP + && isSpiEqual(pkt, IP4_HDRLEN + UDP_HDRLEN, spi); + } else { + return pkt[IP4_PROTO_OFFSET] == IPPROTO_ESP && isSpiEqual(pkt, IP4_HDRLEN, spi); + } + } + } + + public static boolean isIpv6(byte[] pkt) { + // First nibble shows IP version. 0x60 for IPv6 + return (pkt[0] & (byte) 0xF0) == (byte) 0x60; + } + + private static byte[] getReflectedPacket(byte[] pkt) { + byte[] reflected = Arrays.copyOf(pkt, pkt.length); + + if (isIpv6(pkt)) { + // Set reflected packet's dst to that of the original's src + System.arraycopy( + pkt, // src + IP6_ADDR_OFFSET + IP6_ADDR_LEN, // src offset + reflected, // dst + IP6_ADDR_OFFSET, // dst offset + IP6_ADDR_LEN); // len + // Set reflected packet's src IP to that of the original's dst IP + System.arraycopy( + pkt, // src + IP6_ADDR_OFFSET, // src offset + reflected, // dst + IP6_ADDR_OFFSET + IP6_ADDR_LEN, // dst offset + IP6_ADDR_LEN); // len + } else { + // Set reflected packet's dst to that of the original's src + System.arraycopy( + pkt, // src + IP4_ADDR_OFFSET + IP4_ADDR_LEN, // src offset + reflected, // dst + IP4_ADDR_OFFSET, // dst offset + IP4_ADDR_LEN); // len + // Set reflected packet's src IP to that of the original's dst IP + System.arraycopy( + pkt, // src + IP4_ADDR_OFFSET, // src offset + reflected, // dst + IP4_ADDR_OFFSET + IP4_ADDR_LEN, // dst offset + IP4_ADDR_LEN); // len + } + return reflected; + } + + /** Takes all captured packets, flips the src/dst, and re-injects them. */ + public void reflectPackets() throws IOException { + synchronized (mPackets) { + for (byte[] pkt : mPackets) { + injectPacket(getReflectedPacket(pkt)); + } + } + } + + public void injectPacket(byte[] pkt) throws IOException { + FileOutputStream out = new FileOutputStream(mTunFd.getFileDescriptor()); + out.write(pkt); + out.flush(); + } + + /** Resets the intercepted packets. */ + public void reset() throws IOException { + synchronized (mPackets) { + mPackets.clear(); + } + } +} diff --git a/tests/cts/net/src/android/net/cts/UriTest.java b/tests/cts/net/src/android/net/cts/UriTest.java new file mode 100644 index 0000000000..40b8fb7259 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/UriTest.java @@ -0,0 +1,590 @@ +/* + * Copyright (C) 2008 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.cts; + +import android.content.ContentUris; +import android.net.Uri; +import android.os.Parcel; +import android.test.AndroidTestCase; +import java.io.File; +import java.util.Arrays; +import java.util.ArrayList; + +public class UriTest extends AndroidTestCase { + public void testParcelling() { + parcelAndUnparcel(Uri.parse("foo:bob%20lee")); + parcelAndUnparcel(Uri.fromParts("foo", "bob lee", "fragment")); + parcelAndUnparcel(new Uri.Builder() + .scheme("http") + .authority("crazybob.org") + .path("/rss/") + .encodedQuery("a=b") + .fragment("foo") + .build()); + } + + private void parcelAndUnparcel(Uri u) { + Parcel p = Parcel.obtain(); + Uri.writeToParcel(p, u); + p.setDataPosition(0); + assertEquals(u, Uri.CREATOR.createFromParcel(p)); + + p.setDataPosition(0); + u = u.buildUpon().build(); + Uri.writeToParcel(p, u); + p.setDataPosition(0); + assertEquals(u, Uri.CREATOR.createFromParcel(p)); + } + + public void testBuildUpon() { + Uri u = Uri.parse("bob:lee").buildUpon().scheme("robert").build(); + assertEquals("robert", u.getScheme()); + assertEquals("lee", u.getEncodedSchemeSpecificPart()); + assertEquals("lee", u.getSchemeSpecificPart()); + assertNull(u.getQuery()); + assertNull(u.getPath()); + assertNull(u.getAuthority()); + assertNull(u.getHost()); + + Uri a = Uri.fromParts("foo", "bar", "tee"); + Uri b = a.buildUpon().fragment("new").build(); + assertEquals("new", b.getFragment()); + assertEquals("bar", b.getSchemeSpecificPart()); + assertEquals("foo", b.getScheme()); + a = new Uri.Builder() + .scheme("foo") + .encodedOpaquePart("bar") + .fragment("tee") + .build(); + b = a.buildUpon().fragment("new").build(); + assertEquals("new", b.getFragment()); + assertEquals("bar", b.getSchemeSpecificPart()); + assertEquals("foo", b.getScheme()); + + a = Uri.fromParts("scheme", "[2001:db8::dead:e1f]/foo", "bar"); + b = a.buildUpon().fragment("qux").build(); + assertEquals("qux", b.getFragment()); + assertEquals("[2001:db8::dead:e1f]/foo", b.getSchemeSpecificPart()); + assertEquals("scheme", b.getScheme()); + } + + public void testStringUri() { + assertEquals("bob lee", + Uri.parse("foo:bob%20lee").getSchemeSpecificPart()); + assertEquals("bob%20lee", + Uri.parse("foo:bob%20lee").getEncodedSchemeSpecificPart()); + + assertEquals("/bob%20lee", + Uri.parse("foo:/bob%20lee").getEncodedPath()); + assertNull(Uri.parse("foo:bob%20lee").getPath()); + + assertEquals("bob%20lee", + Uri.parse("foo:?bob%20lee").getEncodedQuery()); + assertNull(Uri.parse("foo:bob%20lee").getEncodedQuery()); + assertNull(Uri.parse("foo:bar#?bob%20lee").getQuery()); + + assertEquals("bob%20lee", + Uri.parse("foo:#bob%20lee").getEncodedFragment()); + + Uri uri = Uri.parse("http://localhost:42"); + assertEquals("localhost", uri.getHost()); + assertEquals(42, uri.getPort()); + + uri = Uri.parse("http://bob@localhost:42"); + assertEquals("bob", uri.getUserInfo()); + assertEquals("localhost", uri.getHost()); + assertEquals(42, uri.getPort()); + + uri = Uri.parse("http://bob%20lee@localhost:42"); + assertEquals("bob lee", uri.getUserInfo()); + assertEquals("bob%20lee", uri.getEncodedUserInfo()); + + uri = Uri.parse("http://localhost"); + assertEquals("localhost", uri.getHost()); + assertEquals(-1, uri.getPort()); + + uri = Uri.parse("http://a:a@example.com:a@example2.com/path"); + assertEquals("a:a@example.com:a@example2.com", uri.getAuthority()); + assertEquals("example2.com", uri.getHost()); + assertEquals(-1, uri.getPort()); + assertEquals("/path", uri.getPath()); + + uri = Uri.parse("http://a.foo.com\\.example.com/path"); + assertEquals("a.foo.com", uri.getHost()); + assertEquals(-1, uri.getPort()); + assertEquals("\\.example.com/path", uri.getPath()); + + uri = Uri.parse("https://[2001:db8::dead:e1f]/foo"); + assertEquals("[2001:db8::dead:e1f]", uri.getAuthority()); + assertNull(uri.getUserInfo()); + assertEquals("[2001:db8::dead:e1f]", uri.getHost()); + assertEquals(-1, uri.getPort()); + assertEquals("/foo", uri.getPath()); + assertEquals(null, uri.getFragment()); + assertEquals("//[2001:db8::dead:e1f]/foo", uri.getSchemeSpecificPart()); + + uri = Uri.parse("https://[2001:db8::dead:e1f]/#foo"); + assertEquals("[2001:db8::dead:e1f]", uri.getAuthority()); + assertNull(uri.getUserInfo()); + assertEquals("[2001:db8::dead:e1f]", uri.getHost()); + assertEquals(-1, uri.getPort()); + assertEquals("/", uri.getPath()); + assertEquals("foo", uri.getFragment()); + assertEquals("//[2001:db8::dead:e1f]/", uri.getSchemeSpecificPart()); + + uri = Uri.parse( + "https://some:user@[2001:db8::dead:e1f]:1234/foo?corge=thud&corge=garp#bar"); + assertEquals("some:user@[2001:db8::dead:e1f]:1234", uri.getAuthority()); + assertEquals("some:user", uri.getUserInfo()); + assertEquals("[2001:db8::dead:e1f]", uri.getHost()); + assertEquals(1234, uri.getPort()); + assertEquals("/foo", uri.getPath()); + assertEquals("bar", uri.getFragment()); + assertEquals("//some:user@[2001:db8::dead:e1f]:1234/foo?corge=thud&corge=garp", + uri.getSchemeSpecificPart()); + assertEquals("corge=thud&corge=garp", uri.getQuery()); + assertEquals("thud", uri.getQueryParameter("corge")); + assertEquals(Arrays.asList("thud", "garp"), uri.getQueryParameters("corge")); + } + + public void testCompareTo() { + Uri a = Uri.parse("foo:a"); + Uri b = Uri.parse("foo:b"); + Uri b2 = Uri.parse("foo:b"); + + assertTrue(a.compareTo(b) < 0); + assertTrue(b.compareTo(a) > 0); + assertEquals(0, b.compareTo(b2)); + } + + public void testEqualsAndHashCode() { + Uri a = Uri.parse("http://crazybob.org/test/?foo=bar#tee"); + + Uri b = new Uri.Builder() + .scheme("http") + .authority("crazybob.org") + .path("/test/") + .encodedQuery("foo=bar") + .fragment("tee") + .build(); + + // Try alternate builder methods. + Uri c = new Uri.Builder() + .scheme("http") + .encodedAuthority("crazybob.org") + .encodedPath("/test/") + .encodedQuery("foo=bar") + .encodedFragment("tee") + .build(); + + assertFalse(Uri.EMPTY.equals(null)); + assertEquals(a, b); + assertEquals(b, c); + assertEquals(c, a); + assertEquals(a.hashCode(), b.hashCode()); + assertEquals(b.hashCode(), c.hashCode()); + } + + public void testEncodeAndDecode() { + String encoded = Uri.encode("Bob:/", "/"); + assertEquals(-1, encoded.indexOf(':')); + assertTrue(encoded.indexOf('/') > -1); + assertEncodeDecodeRoundtripExact(null); + assertEncodeDecodeRoundtripExact(""); + assertEncodeDecodeRoundtripExact("Bob"); + assertEncodeDecodeRoundtripExact(":Bob"); + assertEncodeDecodeRoundtripExact("::Bob"); + assertEncodeDecodeRoundtripExact("Bob::Lee"); + assertEncodeDecodeRoundtripExact("Bob:Lee"); + assertEncodeDecodeRoundtripExact("Bob::"); + assertEncodeDecodeRoundtripExact("Bob:"); + assertEncodeDecodeRoundtripExact("::Bob::"); + assertEncodeDecodeRoundtripExact("https:/some:user@[2001:db8::dead:e1f]:1234/foo#bar"); + } + + private static void assertEncodeDecodeRoundtripExact(String s) { + assertEquals(s, Uri.decode(Uri.encode(s, null))); + } + + public void testDecode_emptyString_returnsEmptyString() { + assertEquals("", Uri.decode("")); + } + + public void testDecode_null_returnsNull() { + assertNull(Uri.decode(null)); + } + + public void testDecode_wrongHexDigit() { + // %p in the end. + assertEquals("ab/$\u0102%\u0840\uFFFD\u0000", Uri.decode("ab%2f$%C4%82%25%e0%a1%80%p")); + } + + public void testDecode_secondHexDigitWrong() { + // %1p in the end. + assertEquals("ab/$\u0102%\u0840\uFFFD\u0001", Uri.decode("ab%2f$%c4%82%25%e0%a1%80%1p")); + } + + public void testDecode_endsWithPercent_appendsUnknownCharacter() { + // % in the end. + assertEquals("ab/$\u0102%\u0840\uFFFD", Uri.decode("ab%2f$%c4%82%25%e0%a1%80%")); + } + + public void testDecode_plusNotConverted() { + assertEquals("ab/$\u0102%+\u0840", Uri.decode("ab%2f$%c4%82%25+%e0%a1%80")); + } + + // Last character needs decoding (make sure we are flushing the buffer with chars to decode). + public void testDecode_lastCharacter() { + assertEquals("ab/$\u0102%\u0840", Uri.decode("ab%2f$%c4%82%25%e0%a1%80")); + } + + // Check that a second row of encoded characters is decoded properly (internal buffers are + // reset properly). + public void testDecode_secondRowOfEncoded() { + assertEquals("ab/$\u0102%\u0840aa\u0840", + Uri.decode("ab%2f$%c4%82%25%e0%a1%80aa%e0%a1%80")); + } + + public void testFromFile() { + File f = new File("/tmp/bob"); + Uri uri = Uri.fromFile(f); + assertEquals("file:///tmp/bob", uri.toString()); + try { + Uri.fromFile(null); + fail("testFile fail"); + } catch (NullPointerException e) {} + } + + public void testQueryParameters() { + Uri uri = Uri.parse("content://user"); + assertEquals(null, uri.getQueryParameter("a")); + + uri = uri.buildUpon().appendQueryParameter("a", "b").build(); + assertEquals("b", uri.getQueryParameter("a")); + + uri = uri.buildUpon().appendQueryParameter("a", "b2").build(); + assertEquals(Arrays.asList("b", "b2"), uri.getQueryParameters("a")); + + uri = uri.buildUpon().appendQueryParameter("c", "d").build(); + assertEquals(Arrays.asList("b", "b2"), uri.getQueryParameters("a")); + assertEquals("d", uri.getQueryParameter("c")); + } + + public void testPathOperations() { + Uri uri = Uri.parse("content://user/a/b"); + + assertEquals(2, uri.getPathSegments().size()); + assertEquals("a", uri.getPathSegments().get(0)); + assertEquals("b", uri.getPathSegments().get(1)); + assertEquals("b", uri.getLastPathSegment()); + + Uri first = uri; + uri = uri.buildUpon().appendPath("c").build(); + assertEquals(3, uri.getPathSegments().size()); + assertEquals("c", uri.getPathSegments().get(2)); + assertEquals("c", uri.getLastPathSegment()); + assertEquals("content://user/a/b/c", uri.toString()); + + uri = ContentUris.withAppendedId(uri, 100); + assertEquals(4, uri.getPathSegments().size()); + assertEquals("100", uri.getPathSegments().get(3)); + assertEquals("100", uri.getLastPathSegment()); + assertEquals(100, ContentUris.parseId(uri)); + assertEquals("content://user/a/b/c/100", uri.toString()); + + // Make sure the original URI is still intact. + assertEquals(2, first.getPathSegments().size()); + assertEquals("b", first.getLastPathSegment()); + + try { + first.getPathSegments().get(2); + fail("test path operations"); + } catch (IndexOutOfBoundsException e) {} + + assertEquals(null, Uri.EMPTY.getLastPathSegment()); + + Uri withC = Uri.parse("foo:/a/b/").buildUpon().appendPath("c").build(); + assertEquals("/a/b/c", withC.getPath()); + } + + public void testOpaqueUri() { + Uri uri = Uri.parse("mailto:nobody"); + testOpaqueUri(uri); + + uri = uri.buildUpon().build(); + testOpaqueUri(uri); + + uri = Uri.fromParts("mailto", "nobody", null); + testOpaqueUri(uri); + + uri = uri.buildUpon().build(); + testOpaqueUri(uri); + + uri = new Uri.Builder() + .scheme("mailto") + .opaquePart("nobody") + .build(); + testOpaqueUri(uri); + + uri = uri.buildUpon().build(); + testOpaqueUri(uri); + } + + private void testOpaqueUri(Uri uri) { + assertEquals("mailto", uri.getScheme()); + assertEquals("nobody", uri.getSchemeSpecificPart()); + assertEquals("nobody", uri.getEncodedSchemeSpecificPart()); + + assertNull(uri.getFragment()); + assertTrue(uri.isAbsolute()); + assertTrue(uri.isOpaque()); + assertFalse(uri.isRelative()); + assertFalse(uri.isHierarchical()); + + assertNull(uri.getAuthority()); + assertNull(uri.getEncodedAuthority()); + assertNull(uri.getPath()); + assertNull(uri.getEncodedPath()); + assertNull(uri.getUserInfo()); + assertNull(uri.getEncodedUserInfo()); + assertNull(uri.getQuery()); + assertNull(uri.getEncodedQuery()); + assertNull(uri.getHost()); + assertEquals(-1, uri.getPort()); + + assertTrue(uri.getPathSegments().isEmpty()); + assertNull(uri.getLastPathSegment()); + + assertEquals("mailto:nobody", uri.toString()); + + Uri withFragment = uri.buildUpon().fragment("top").build(); + assertEquals("mailto:nobody#top", withFragment.toString()); + } + + public void testHierarchicalUris() { + testHierarchical("http", "google.com", "/p1/p2", "query", "fragment"); + testHierarchical("file", null, "/p1/p2", null, null); + testHierarchical("content", "contact", "/p1/p2", null, null); + testHierarchical("http", "google.com", "/p1/p2", null, "fragment"); + testHierarchical("http", "google.com", "", null, "fragment"); + testHierarchical("http", "google.com", "", "query", "fragment"); + testHierarchical("http", "google.com", "", "query", null); + testHierarchical("http", null, "/", "query", null); + } + + private static void testHierarchical(String scheme, String authority, + String path, String query, String fragment) { + StringBuilder sb = new StringBuilder(); + + if (authority != null) { + sb.append("//").append(authority); + } + if (path != null) { + sb.append(path); + } + if (query != null) { + sb.append('?').append(query); + } + + String ssp = sb.toString(); + + if (scheme != null) { + sb.insert(0, scheme + ":"); + } + if (fragment != null) { + sb.append('#').append(fragment); + } + + String uriString = sb.toString(); + + Uri uri = Uri.parse(uriString); + + // Run these twice to test caching. + compareHierarchical( + uriString, ssp, uri, scheme, authority, path, query, fragment); + compareHierarchical( + uriString, ssp, uri, scheme, authority, path, query, fragment); + + // Test rebuilt version. + uri = uri.buildUpon().build(); + + // Run these twice to test caching. + compareHierarchical( + uriString, ssp, uri, scheme, authority, path, query, fragment); + compareHierarchical( + uriString, ssp, uri, scheme, authority, path, query, fragment); + + // The decoded and encoded versions of the inputs are all the same. + // We'll test the actual encoding decoding separately. + + // Test building with encoded versions. + Uri built = new Uri.Builder() + .scheme(scheme) + .encodedAuthority(authority) + .encodedPath(path) + .encodedQuery(query) + .encodedFragment(fragment) + .build(); + + compareHierarchical( + uriString, ssp, built, scheme, authority, path, query, fragment); + compareHierarchical( + uriString, ssp, built, scheme, authority, path, query, fragment); + + // Test building with decoded versions. + built = new Uri.Builder() + .scheme(scheme) + .authority(authority) + .path(path) + .query(query) + .fragment(fragment) + .build(); + + compareHierarchical( + uriString, ssp, built, scheme, authority, path, query, fragment); + compareHierarchical( + uriString, ssp, built, scheme, authority, path, query, fragment); + + // Rebuild. + built = built.buildUpon().build(); + + compareHierarchical( + uriString, ssp, built, scheme, authority, path, query, fragment); + compareHierarchical( + uriString, ssp, built, scheme, authority, path, query, fragment); + } + + private static void compareHierarchical(String uriString, String ssp, + Uri uri, + String scheme, String authority, String path, String query, + String fragment) { + assertEquals(scheme, uri.getScheme()); + assertEquals(authority, uri.getAuthority()); + assertEquals(authority, uri.getEncodedAuthority()); + assertEquals(path, uri.getPath()); + assertEquals(path, uri.getEncodedPath()); + assertEquals(query, uri.getQuery()); + assertEquals(query, uri.getEncodedQuery()); + assertEquals(fragment, uri.getFragment()); + assertEquals(fragment, uri.getEncodedFragment()); + assertEquals(ssp, uri.getSchemeSpecificPart()); + + if (scheme != null) { + assertTrue(uri.isAbsolute()); + assertFalse(uri.isRelative()); + } else { + assertFalse(uri.isAbsolute()); + assertTrue(uri.isRelative()); + } + + assertFalse(uri.isOpaque()); + assertTrue(uri.isHierarchical()); + assertEquals(uriString, uri.toString()); + } + + public void testNormalizeScheme() { + assertEquals(Uri.parse(""), Uri.parse("").normalizeScheme()); + assertEquals(Uri.parse("http://www.android.com"), + Uri.parse("http://www.android.com").normalizeScheme()); + assertEquals(Uri.parse("http://USER@WWW.ANDROID.COM:100/ABOUT?foo=blah@bar=bleh#c"), + Uri.parse("HTTP://USER@WWW.ANDROID.COM:100/ABOUT?foo=blah@bar=bleh#c") + .normalizeScheme()); + } + + public void testToSafeString_tel() { + checkToSafeString("tel:xxxxxx", "tel:Google"); + checkToSafeString("tel:xxxxxxxxxx", "tel:1234567890"); + checkToSafeString("tEl:xxx.xxx-xxxx", "tEl:123.456-7890"); + } + + public void testToSafeString_sip() { + checkToSafeString("sip:xxxxxxx@xxxxxxx.xxxxxxxx", "sip:android@android.com:1234"); + checkToSafeString("sIp:xxxxxxx@xxxxxxx.xxx", "sIp:android@android.com"); + } + + public void testToSafeString_sms() { + checkToSafeString("sms:xxxxxx", "sms:123abc"); + checkToSafeString("smS:xxx.xxx-xxxx", "smS:123.456-7890"); + } + + public void testToSafeString_smsto() { + checkToSafeString("smsto:xxxxxx", "smsto:123abc"); + checkToSafeString("SMSTo:xxx.xxx-xxxx", "SMSTo:123.456-7890"); + } + + public void testToSafeString_mailto() { + checkToSafeString("mailto:xxxxxxx@xxxxxxx.xxx", "mailto:android@android.com"); + checkToSafeString("Mailto:xxxxxxx@xxxxxxx.xxxxxxxxxx", + "Mailto:android@android.com/secret"); + } + + public void testToSafeString_nfc() { + checkToSafeString("nfc:xxxxxx", "nfc:123abc"); + checkToSafeString("nfc:xxx.xxx-xxxx", "nfc:123.456-7890"); + checkToSafeString("nfc:xxxxxxx@xxxxxxx.xxx", "nfc:android@android.com"); + } + + public void testToSafeString_http() { + checkToSafeString("http://www.android.com/...", "http://www.android.com"); + checkToSafeString("HTTP://www.android.com/...", "HTTP://www.android.com"); + checkToSafeString("http://www.android.com/...", "http://www.android.com/"); + checkToSafeString("http://www.android.com/...", "http://www.android.com/secretUrl?param"); + checkToSafeString("http://www.android.com/...", + "http://user:pwd@www.android.com/secretUrl?param"); + checkToSafeString("http://www.android.com/...", + "http://user@www.android.com/secretUrl?param"); + checkToSafeString("http://www.android.com/...", "http://www.android.com/secretUrl?param"); + checkToSafeString("http:///...", "http:///path?param"); + checkToSafeString("http:///...", "http://"); + checkToSafeString("http://:12345/...", "http://:12345/"); + } + + public void testToSafeString_https() { + checkToSafeString("https://www.android.com/...", "https://www.android.com/secretUrl?param"); + checkToSafeString("https://www.android.com:8443/...", + "https://user:pwd@www.android.com:8443/secretUrl?param"); + checkToSafeString("https://www.android.com/...", "https://user:pwd@www.android.com"); + checkToSafeString("Https://www.android.com/...", "Https://user:pwd@www.android.com"); + } + + public void testToSafeString_ftp() { + checkToSafeString("ftp://ftp.android.com/...", "ftp://ftp.android.com/"); + checkToSafeString("ftP://ftp.android.com/...", "ftP://anonymous@ftp.android.com/"); + checkToSafeString("ftp://ftp.android.com:2121/...", + "ftp://root:love@ftp.android.com:2121/"); + } + + public void testToSafeString_rtsp() { + checkToSafeString("rtsp://rtsp.android.com/...", "rtsp://rtsp.android.com/"); + checkToSafeString("rtsp://rtsp.android.com/...", "rtsp://rtsp.android.com/video.mov"); + checkToSafeString("rtsp://rtsp.android.com/...", "rtsp://rtsp.android.com/video.mov?param"); + checkToSafeString("RtsP://rtsp.android.com/...", "RtsP://anonymous@rtsp.android.com/"); + checkToSafeString("rtsp://rtsp.android.com:2121/...", + "rtsp://username:password@rtsp.android.com:2121/"); + } + + public void testToSafeString_notSupport() { + checkToSafeString("unsupported://ajkakjah/askdha/secret?secret", + "unsupported://ajkakjah/askdha/secret?secret"); + checkToSafeString("unsupported:ajkakjah/askdha/secret?secret", + "unsupported:ajkakjah/askdha/secret?secret"); + } + + private void checkToSafeString(String expectedSafeString, String original) { + assertEquals(expectedSafeString, Uri.parse(original).toSafeString()); + } +} diff --git a/tests/cts/net/src/android/net/cts/Uri_BuilderTest.java b/tests/cts/net/src/android/net/cts/Uri_BuilderTest.java new file mode 100644 index 0000000000..4088d822cf --- /dev/null +++ b/tests/cts/net/src/android/net/cts/Uri_BuilderTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2008 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.cts; + +import junit.framework.TestCase; +import android.net.Uri.Builder; +import android.net.Uri; + +public class Uri_BuilderTest extends TestCase { + public void testBuilderOperations() { + Uri uri = Uri.parse("http://google.com/p1?query#fragment"); + Builder builder = uri.buildUpon(); + uri = builder.appendPath("p2").build(); + assertEquals("http", uri.getScheme()); + assertEquals("google.com", uri.getAuthority()); + assertEquals("/p1/p2", uri.getPath()); + assertEquals("query", uri.getQuery()); + assertEquals("fragment", uri.getFragment()); + assertEquals(uri.toString(), builder.toString()); + + uri = Uri.parse("mailto:nobody"); + builder = uri.buildUpon(); + uri = builder.build(); + assertEquals("mailto", uri.getScheme()); + assertEquals("nobody", uri.getSchemeSpecificPart()); + assertEquals(uri.toString(), builder.toString()); + + uri = new Uri.Builder() + .scheme("http") + .encodedAuthority("google.com") + .encodedPath("/p1") + .appendEncodedPath("p2") + .encodedQuery("query") + .appendQueryParameter("query2", null) + .encodedFragment("fragment") + .build(); + assertEquals("http", uri.getScheme()); + assertEquals("google.com", uri.getEncodedAuthority()); + assertEquals("/p1/p2", uri.getEncodedPath()); + assertEquals("query&query2=null", uri.getEncodedQuery()); + assertEquals("fragment", uri.getEncodedFragment()); + + uri = new Uri.Builder() + .scheme("mailto") + .encodedOpaquePart("nobody") + .build(); + assertEquals("mailto", uri.getScheme()); + assertEquals("nobody", uri.getEncodedSchemeSpecificPart()); + } +} diff --git a/tests/cts/net/src/android/net/cts/UrlQuerySanitizerTest.java b/tests/cts/net/src/android/net/cts/UrlQuerySanitizerTest.java new file mode 100644 index 0000000000..5a70928e37 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/UrlQuerySanitizerTest.java @@ -0,0 +1,290 @@ +/* + * 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.cts; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import android.net.UrlQuerySanitizer; +import android.net.UrlQuerySanitizer.IllegalCharacterValueSanitizer; +import android.net.UrlQuerySanitizer.ParameterValuePair; +import android.net.UrlQuerySanitizer.ValueSanitizer; +import android.os.Build; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +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 java.util.List; +import java.util.Set; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class UrlQuerySanitizerTest { + @Rule + public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(); + + private static final int ALL_OK = IllegalCharacterValueSanitizer.ALL_OK; + + // URL for test. + private static final String TEST_URL = "http://example.com/?name=Joe+User&age=20&height=175"; + + // Default sanitizer's change when "+". + private static final String EXPECTED_UNDERLINE_NAME = "Joe_User"; + + // IllegalCharacterValueSanitizer sanitizer's change when "+". + private static final String EXPECTED_SPACE_NAME = "Joe User"; + private static final String EXPECTED_AGE = "20"; + private static final String EXPECTED_HEIGHT = "175"; + private static final String NAME = "name"; + private static final String AGE = "age"; + private static final String HEIGHT = "height"; + + @Test + public void testUrlQuerySanitizer() { + MockUrlQuerySanitizer uqs = new MockUrlQuerySanitizer(); + assertFalse(uqs.getAllowUnregisteredParamaters()); + + final String query = "book=thinking in java&price=108"; + final String book = "book"; + final String bookName = "thinking in java"; + final String price = "price"; + final String bookPrice = "108"; + final String notExistPar = "notExistParameter"; + uqs.registerParameters(new String[]{book, price}, UrlQuerySanitizer.getSpaceLegal()); + uqs.parseQuery(query); + assertTrue(uqs.hasParameter(book)); + assertTrue(uqs.hasParameter(price)); + assertFalse(uqs.hasParameter(notExistPar)); + assertEquals(bookName, uqs.getValue(book)); + assertEquals(bookPrice, uqs.getValue(price)); + assertNull(uqs.getValue(notExistPar)); + uqs.clear(); + assertFalse(uqs.hasParameter(book)); + assertFalse(uqs.hasParameter(price)); + + uqs.parseEntry(book, bookName); + assertTrue(uqs.hasParameter(book)); + assertEquals(bookName, uqs.getValue(book)); + uqs.parseEntry(price, bookPrice); + assertTrue(uqs.hasParameter(price)); + assertEquals(bookPrice, uqs.getValue(price)); + assertFalse(uqs.hasParameter(notExistPar)); + assertNull(uqs.getValue(notExistPar)); + + uqs = new MockUrlQuerySanitizer(TEST_URL); + assertTrue(uqs.getAllowUnregisteredParamaters()); + + assertTrue(uqs.hasParameter(NAME)); + assertTrue(uqs.hasParameter(AGE)); + assertTrue(uqs.hasParameter(HEIGHT)); + assertFalse(uqs.hasParameter(notExistPar)); + + assertEquals(EXPECTED_UNDERLINE_NAME, uqs.getValue(NAME)); + assertEquals(EXPECTED_AGE, uqs.getValue(AGE)); + assertEquals(EXPECTED_HEIGHT, uqs.getValue(HEIGHT)); + assertNull(uqs.getValue(notExistPar)); + + final int ContainerLen = 3; + Set urlSet = uqs.getParameterSet(); + assertEquals(ContainerLen, urlSet.size()); + assertTrue(urlSet.contains(NAME)); + assertTrue(urlSet.contains(AGE)); + assertTrue(urlSet.contains(HEIGHT)); + assertFalse(urlSet.contains(notExistPar)); + + List urlList = uqs.getParameterList(); + assertEquals(ContainerLen, urlList.size()); + ParameterValuePair pvp = urlList.get(0); + assertEquals(NAME, pvp.mParameter); + assertEquals(EXPECTED_UNDERLINE_NAME, pvp.mValue); + pvp = urlList.get(1); + assertEquals(AGE, pvp.mParameter); + assertEquals(EXPECTED_AGE, pvp.mValue); + pvp = urlList.get(2); + assertEquals(HEIGHT, pvp.mParameter); + assertEquals(EXPECTED_HEIGHT, pvp.mValue); + + assertFalse(uqs.getPreferFirstRepeatedParameter()); + uqs.addSanitizedEntry(HEIGHT, EXPECTED_HEIGHT + 1); + assertEquals(ContainerLen, urlSet.size()); + assertEquals(ContainerLen + 1, urlList.size()); + assertEquals(EXPECTED_HEIGHT + 1, uqs.getValue(HEIGHT)); + + uqs.setPreferFirstRepeatedParameter(true); + assertTrue(uqs.getPreferFirstRepeatedParameter()); + uqs.addSanitizedEntry(HEIGHT, EXPECTED_HEIGHT); + assertEquals(ContainerLen, urlSet.size()); + assertEquals(ContainerLen + 2, urlList.size()); + assertEquals(EXPECTED_HEIGHT + 1, uqs.getValue(HEIGHT)); + + uqs.registerParameter(NAME, null); + assertNull(uqs.getValueSanitizer(NAME)); + assertNotNull(uqs.getEffectiveValueSanitizer(NAME)); + + uqs.setAllowUnregisteredParamaters(false); + assertFalse(uqs.getAllowUnregisteredParamaters()); + uqs.registerParameter(NAME, null); + assertNull(uqs.getEffectiveValueSanitizer(NAME)); + + ValueSanitizer vs = new IllegalCharacterValueSanitizer(ALL_OK); + uqs.registerParameter(NAME, vs); + uqs.parseUrl(TEST_URL); + assertEquals(EXPECTED_SPACE_NAME, uqs.getValue(NAME)); + assertNotSame(EXPECTED_AGE, uqs.getValue(AGE)); + + String[] register = {NAME, AGE}; + uqs.registerParameters(register, vs); + uqs.parseUrl(TEST_URL); + assertEquals(EXPECTED_SPACE_NAME, uqs.getValue(NAME)); + assertEquals(EXPECTED_AGE, uqs.getValue(AGE)); + assertNotSame(EXPECTED_HEIGHT, uqs.getValue(HEIGHT)); + + uqs.setUnregisteredParameterValueSanitizer(vs); + assertEquals(vs, uqs.getUnregisteredParameterValueSanitizer()); + + vs = UrlQuerySanitizer.getAllIllegal(); + assertEquals("Joe_User", vs.sanitize("Joe\0User")); + vs = UrlQuerySanitizer.getAllButNulLegal(); + assertEquals("Joe User", vs.sanitize("Joe\0User")); + vs = UrlQuerySanitizer.getAllButWhitespaceLegal(); + assertEquals("Joe_User", vs.sanitize("Joe User")); + vs = UrlQuerySanitizer.getAmpAndSpaceLegal(); + assertEquals("Joe User&", vs.sanitize("Joe User&")); + vs = UrlQuerySanitizer.getAmpLegal(); + assertEquals("Joe_User&", vs.sanitize("Joe User&")); + vs = UrlQuerySanitizer.getSpaceLegal(); + assertEquals("Joe User ", vs.sanitize("Joe User&")); + vs = UrlQuerySanitizer.getUrlAndSpaceLegal(); + assertEquals("Joe User&Smith%B5'\'", vs.sanitize("Joe User&Smith%B5'\'")); + vs = UrlQuerySanitizer.getUrlLegal(); + assertEquals("Joe_User&Smith%B5'\'", vs.sanitize("Joe User&Smith%B5'\'")); + + String escape = "Joe"; + assertEquals(escape, uqs.unescape(escape)); + String expectedPlus = "Joe User"; + String expectedPercentSignHex = "title=" + Character.toString((char)181); + String initialPlus = "Joe+User"; + String initialPercentSign = "title=%B5"; + assertEquals(expectedPlus, uqs.unescape(initialPlus)); + assertEquals(expectedPercentSignHex, uqs.unescape(initialPercentSign)); + String expectedPlusThenPercentSign = "Joe Random, User"; + String plusThenPercentSign = "Joe+Random%2C%20User"; + assertEquals(expectedPlusThenPercentSign, uqs.unescape(plusThenPercentSign)); + String expectedPercentSignThenPlus = "Joe, Random User"; + String percentSignThenPlus = "Joe%2C+Random+User"; + assertEquals(expectedPercentSignThenPlus, uqs.unescape(percentSignThenPlus)); + + assertTrue(uqs.decodeHexDigit('0') >= 0); + assertTrue(uqs.decodeHexDigit('b') >= 0); + assertTrue(uqs.decodeHexDigit('F') >= 0); + assertTrue(uqs.decodeHexDigit('$') < 0); + + assertTrue(uqs.isHexDigit('0')); + assertTrue(uqs.isHexDigit('b')); + assertTrue(uqs.isHexDigit('F')); + assertFalse(uqs.isHexDigit('$')); + + uqs.clear(); + assertEquals(0, urlSet.size()); + assertEquals(0, urlList.size()); + + uqs.setPreferFirstRepeatedParameter(true); + assertTrue(uqs.getPreferFirstRepeatedParameter()); + uqs.setPreferFirstRepeatedParameter(false); + assertFalse(uqs.getPreferFirstRepeatedParameter()); + + UrlQuerySanitizer uq = new UrlQuerySanitizer(); + uq.setPreferFirstRepeatedParameter(true); + final String PARA_ANSWER = "answer"; + uq.registerParameter(PARA_ANSWER, new MockValueSanitizer()); + uq.parseUrl("http://www.google.com/question?answer=13&answer=42"); + assertEquals("13", uq.getValue(PARA_ANSWER)); + + uq.setPreferFirstRepeatedParameter(false); + uq.parseQuery("http://www.google.com/question?answer=13&answer=42"); + assertEquals("42", uq.getValue(PARA_ANSWER)); + + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) // Only fixed in R + public void testScriptUrlOk_73822755() { + ValueSanitizer sanitizer = new UrlQuerySanitizer.IllegalCharacterValueSanitizer( + UrlQuerySanitizer.IllegalCharacterValueSanitizer.SCRIPT_URL_OK); + assertEquals("javascript:alert()", sanitizer.sanitize("javascript:alert()")); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) // Only fixed in R + public void testScriptUrlBlocked_73822755() { + ValueSanitizer sanitizer = UrlQuerySanitizer.getUrlAndSpaceLegal(); + assertEquals("", sanitizer.sanitize("javascript:alert()")); + } + + private static class MockValueSanitizer implements ValueSanitizer{ + + public String sanitize(String value) { + return value; + } + } + + class MockUrlQuerySanitizer extends UrlQuerySanitizer { + public MockUrlQuerySanitizer() { + super(); + } + + public MockUrlQuerySanitizer(String url) { + super(url); + } + + @Override + protected void addSanitizedEntry(String parameter, String value) { + super.addSanitizedEntry(parameter, value); + } + + @Override + protected void clear() { + super.clear(); + } + + @Override + protected int decodeHexDigit(char c) { + return super.decodeHexDigit(c); + } + + @Override + protected boolean isHexDigit(char c) { + return super.isHexDigit(c); + } + + @Override + protected void parseEntry(String parameter, String value) { + super.parseEntry(parameter, value); + } + } +} diff --git a/tests/cts/net/src/android/net/cts/UrlQuerySanitizer_IllegalCharacterValueSanitizerTest.java b/tests/cts/net/src/android/net/cts/UrlQuerySanitizer_IllegalCharacterValueSanitizerTest.java new file mode 100644 index 0000000000..f86af3114e --- /dev/null +++ b/tests/cts/net/src/android/net/cts/UrlQuerySanitizer_IllegalCharacterValueSanitizerTest.java @@ -0,0 +1,29 @@ +/* + * 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.cts; + +import android.net.UrlQuerySanitizer; +import android.net.UrlQuerySanitizer.IllegalCharacterValueSanitizer; +import android.test.AndroidTestCase; + +public class UrlQuerySanitizer_IllegalCharacterValueSanitizerTest extends AndroidTestCase { + static final int SPACE_OK = IllegalCharacterValueSanitizer.SPACE_OK; + public void testSanitize() { + IllegalCharacterValueSanitizer sanitizer = new IllegalCharacterValueSanitizer(SPACE_OK); + assertEquals("Joe User", sanitizer.sanitize("Joecommon/android-3.x kernel trees. If you are not running one of these kernels, the + * functionality can be obtained by cherry-picking the following patches from David Miller's + * net-next tree: + *

    + *
  • 6d0bfe2 net: ipv6: Add IPv6 support to the ping socket. + *
  • c26d6b4 ping: always initialize ->sin6_scope_id and ->sin6_flowinfo + *
  • fbfe80c net: ipv6: fix wrong ping_v6_sendmsg return value + *
  • a1bdc45 net: ipv6: add missing lock in ping_v6_sendmsg + *
  • cf970c0 ping: prevent NULL pointer dereference on write to msg_name + *
+ * or the equivalent backports to the common/android-3.x trees. + */ +public class PingTest extends AndroidTestCase { + /** Maximum size of the packets we're using to test. */ + private static final int MAX_SIZE = 4096; + + /** Size of the ICMPv6 header. */ + private static final int ICMP_HEADER_SIZE = 8; + + /** Number of packets to test. */ + private static final int NUM_PACKETS = 10; + + /** The beginning of an ICMPv6 echo request: type, code, and uninitialized checksum. */ + private static final byte[] PING_HEADER = new byte[] { + (byte) ICMP6_ECHO_REQUEST, (byte) 0x00, (byte) 0x00, (byte) 0x00 + }; + + /** + * Returns a byte array containing an ICMPv6 echo request with the specified payload length. + */ + private byte[] pingPacket(int payloadLength) { + byte[] packet = new byte[payloadLength + ICMP_HEADER_SIZE]; + new Random().nextBytes(packet); + System.arraycopy(PING_HEADER, 0, packet, 0, PING_HEADER.length); + return packet; + } + + /** + * Checks that the first length bytes of two byte arrays are equal. + */ + private void assertArrayBytesEqual(byte[] expected, byte[] actual, int length) { + for (int i = 0; i < length; i++) { + assertEquals("Arrays differ at index " + i + ":", expected[i], actual[i]); + } + } + + /** + * Creates an IPv6 ping socket and sets a receive timeout of 100ms. + */ + private FileDescriptor createPingSocket() throws ErrnoException { + FileDescriptor s = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6); + Os.setsockoptTimeval(s, SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(100)); + return s; + } + + /** + * Sends a ping packet to a random port on the specified address on the specified socket. + */ + private void sendPing(FileDescriptor s, + InetAddress address, byte[] packet) throws ErrnoException, IOException { + // Pick a random port. Choose a range that gives a reasonable chance of picking a low port. + int port = (int) (Math.random() * 2048); + + // Send the packet. + int ret = Os.sendto(s, ByteBuffer.wrap(packet), 0, address, port); + assertEquals(packet.length, ret); + } + + /** + * Checks that a socket has received a response appropriate to the specified packet. + */ + private void checkResponse(FileDescriptor s, InetAddress dest, + byte[] sent, boolean useRecvfrom) throws ErrnoException, IOException { + ByteBuffer responseBuffer = ByteBuffer.allocate(MAX_SIZE); + int bytesRead; + + // Receive the response. + if (useRecvfrom) { + InetSocketAddress from = new InetSocketAddress(); + bytesRead = Os.recvfrom(s, responseBuffer, 0, from); + + // Check the source address and scope ID. + assertTrue(from.getAddress() instanceof Inet6Address); + Inet6Address fromAddress = (Inet6Address) from.getAddress(); + assertEquals(0, fromAddress.getScopeId()); + assertNull(fromAddress.getScopedInterface()); + assertEquals(dest.getHostAddress(), fromAddress.getHostAddress()); + } else { + bytesRead = Os.read(s, responseBuffer); + } + + // Check the packet length. + assertEquals(sent.length, bytesRead); + + // Check the response is an echo reply. + byte[] response = new byte[bytesRead]; + responseBuffer.flip(); + responseBuffer.get(response, 0, bytesRead); + assertEquals((byte) ICMP6_ECHO_REPLY, response[0]); + + // Find out what ICMP ID was used in the packet that was sent. + int id = ((InetSocketAddress) Os.getsockname(s)).getPort(); + sent[4] = (byte) (id / 256); + sent[5] = (byte) (id % 256); + + // Ensure the response is the same as the packet, except for the type (which is 0x81) + // and the ID and checksum, which are set by the kernel. + response[0] = (byte) 0x80; // Type. + response[2] = response[3] = (byte) 0x00; // Checksum. + assertArrayBytesEqual(response, sent, bytesRead); + } + + /** + * Sends NUM_PACKETS random ping packets to ::1 and checks the replies. + */ + public void testLoopbackPing() throws ErrnoException, IOException { + // Generate a random ping packet and send it to localhost. + InetAddress ipv6Loopback = InetAddress.getByName(null); + assertEquals("::1", ipv6Loopback.getHostAddress()); + + for (int i = 0; i < NUM_PACKETS; i++) { + byte[] packet = pingPacket((int) (Math.random() * (MAX_SIZE - ICMP_HEADER_SIZE))); + FileDescriptor s = createPingSocket(); + // Use both recvfrom and read(). + sendPing(s, ipv6Loopback, packet); + checkResponse(s, ipv6Loopback, packet, true); + sendPing(s, ipv6Loopback, packet); + checkResponse(s, ipv6Loopback, packet, false); + // Check closing the socket doesn't raise an exception. + Os.close(s); + } + } +} diff --git a/tests/cts/net/src/android/net/rtp/cts/AudioCodecTest.java b/tests/cts/net/src/android/net/rtp/cts/AudioCodecTest.java new file mode 100644 index 0000000000..412498c309 --- /dev/null +++ b/tests/cts/net/src/android/net/rtp/cts/AudioCodecTest.java @@ -0,0 +1,73 @@ +/* + * 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 android.net.rtp.cts; + +import android.net.rtp.AudioCodec; +import android.test.AndroidTestCase; + +public class AudioCodecTest extends AndroidTestCase { + + private void assertEquals(AudioCodec codec, int type, String rtpmap, String fmtp) { + if (type >= 0) { + assertEquals(codec.type, type); + } else { + assertTrue(codec.type >= 96 && codec.type <= 127); + } + assertEquals(codec.rtpmap.compareToIgnoreCase(rtpmap), 0); + assertEquals(codec.fmtp, fmtp); + } + + public void testConstants() throws Exception { + assertEquals(AudioCodec.PCMU, 0, "PCMU/8000", null); + assertEquals(AudioCodec.PCMA, 8, "PCMA/8000", null); + assertEquals(AudioCodec.GSM, 3, "GSM/8000", null); + assertEquals(AudioCodec.GSM_EFR, -1, "GSM-EFR/8000", null); + assertEquals(AudioCodec.AMR, -1, "AMR/8000", null); + + assertFalse(AudioCodec.AMR.type == AudioCodec.GSM_EFR.type); + } + + public void testGetCodec() throws Exception { + // Bad types. + assertNull(AudioCodec.getCodec(128, "PCMU/8000", null)); + assertNull(AudioCodec.getCodec(-1, "PCMU/8000", null)); + assertNull(AudioCodec.getCodec(96, null, null)); + + // Fixed types. + assertEquals(AudioCodec.getCodec(0, null, null), 0, "PCMU/8000", null); + assertEquals(AudioCodec.getCodec(8, null, null), 8, "PCMA/8000", null); + assertEquals(AudioCodec.getCodec(3, null, null), 3, "GSM/8000", null); + + // Dynamic types. + assertEquals(AudioCodec.getCodec(96, "pcmu/8000", null), 96, "PCMU/8000", null); + assertEquals(AudioCodec.getCodec(97, "pcma/8000", null), 97, "PCMA/8000", null); + assertEquals(AudioCodec.getCodec(98, "gsm/8000", null), 98, "GSM/8000", null); + assertEquals(AudioCodec.getCodec(99, "gsm-efr/8000", null), 99, "GSM-EFR/8000", null); + assertEquals(AudioCodec.getCodec(100, "amr/8000", null), 100, "AMR/8000", null); + } + + public void testGetCodecs() throws Exception { + AudioCodec[] codecs = AudioCodec.getCodecs(); + assertTrue(codecs.length >= 5); + + // The types of the codecs should be different. + boolean[] types = new boolean[128]; + for (AudioCodec codec : codecs) { + assertFalse(types[codec.type]); + types[codec.type] = true; + } + } +} diff --git a/tests/cts/net/src/android/net/rtp/cts/AudioGroupTest.java b/tests/cts/net/src/android/net/rtp/cts/AudioGroupTest.java new file mode 100644 index 0000000000..fc78e96e11 --- /dev/null +++ b/tests/cts/net/src/android/net/rtp/cts/AudioGroupTest.java @@ -0,0 +1,177 @@ +/* + * 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 android.net.rtp.cts; + +import android.content.Context; +import android.media.AudioManager; +import android.net.rtp.AudioCodec; +import android.net.rtp.AudioGroup; +import android.net.rtp.AudioStream; +import android.net.rtp.RtpStream; +import android.os.Build; +import android.platform.test.annotations.AppModeFull; +import android.test.AndroidTestCase; + +import androidx.core.os.BuildCompat; + +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; + +@AppModeFull(reason = "RtpStream cannot create in instant app mode") +public class AudioGroupTest extends AndroidTestCase { + + private static final String TAG = AudioGroupTest.class.getSimpleName(); + + private AudioManager mAudioManager; + + private AudioStream mStreamA; + private DatagramSocket mSocketA; + private AudioStream mStreamB; + private DatagramSocket mSocketB; + private AudioGroup mGroup; + + @Override + public void setUp() throws Exception { + mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); + + InetAddress local = InetAddress.getByName("::1"); + + mStreamA = new AudioStream(local); + mStreamA.setMode(RtpStream.MODE_NORMAL); + mStreamA.setCodec(AudioCodec.PCMU); + mSocketA = new DatagramSocket(); + mSocketA.connect(mStreamA.getLocalAddress(), mStreamA.getLocalPort()); + mStreamA.associate(mSocketA.getLocalAddress(), mSocketA.getLocalPort()); + + mStreamB = new AudioStream(local); + mStreamB.setMode(RtpStream.MODE_NORMAL); + mStreamB.setCodec(AudioCodec.PCMU); + mSocketB = new DatagramSocket(); + mSocketB.connect(mStreamB.getLocalAddress(), mStreamB.getLocalPort()); + mStreamB.associate(mSocketB.getLocalAddress(), mSocketB.getLocalPort()); + + // BuildCompat.isAtLeastR is documented to return false on release SDKs (including R) + mGroup = Build.VERSION.SDK_INT > Build.VERSION_CODES.Q || BuildCompat.isAtLeastR() + ? new AudioGroup(mContext) + : new AudioGroup(); // Constructor with context argument was introduced in R + } + + @Override + public void tearDown() throws Exception { + mGroup.clear(); + mStreamA.release(); + mSocketA.close(); + mStreamB.release(); + mSocketB.close(); + mAudioManager.setMode(AudioManager.MODE_NORMAL); + } + + private void assertPacket(DatagramSocket socket, int length) throws Exception { + DatagramPacket packet = new DatagramPacket(new byte[length + 1], length + 1); + socket.setSoTimeout(3000); + socket.receive(packet); + assertEquals(packet.getLength(), length); + } + + private void drain(DatagramSocket socket) throws Exception { + DatagramPacket packet = new DatagramPacket(new byte[1], 1); + socket.setSoTimeout(1); + try { + // Drain the socket by retrieving all the packets queued on it. + // A SocketTimeoutException will be thrown when it becomes empty. + while (true) { + socket.receive(packet); + } + } catch (Exception e) { + // ignore. + } + } + + public void testTraffic() throws Exception { + mStreamA.join(mGroup); + assertPacket(mSocketA, 12 + 160); + + mStreamB.join(mGroup); + assertPacket(mSocketB, 12 + 160); + + mStreamA.join(null); + drain(mSocketA); + + drain(mSocketB); + assertPacket(mSocketB, 12 + 160); + + mStreamA.join(mGroup); + assertPacket(mSocketA, 12 + 160); + } + + public void testSetMode() throws Exception { + mGroup.setMode(AudioGroup.MODE_NORMAL); + assertEquals(mGroup.getMode(), AudioGroup.MODE_NORMAL); + + mGroup.setMode(AudioGroup.MODE_MUTED); + assertEquals(mGroup.getMode(), AudioGroup.MODE_MUTED); + + mStreamA.join(mGroup); + mStreamB.join(mGroup); + + mGroup.setMode(AudioGroup.MODE_NORMAL); + assertEquals(mGroup.getMode(), AudioGroup.MODE_NORMAL); + + mGroup.setMode(AudioGroup.MODE_MUTED); + assertEquals(mGroup.getMode(), AudioGroup.MODE_MUTED); + } + + public void testAdd() throws Exception { + mStreamA.join(mGroup); + assertEquals(mGroup.getStreams().length, 1); + + mStreamB.join(mGroup); + assertEquals(mGroup.getStreams().length, 2); + + mStreamA.join(mGroup); + assertEquals(mGroup.getStreams().length, 2); + } + + public void testRemove() throws Exception { + mStreamA.join(mGroup); + assertEquals(mGroup.getStreams().length, 1); + + mStreamA.join(null); + assertEquals(mGroup.getStreams().length, 0); + + mStreamA.join(mGroup); + assertEquals(mGroup.getStreams().length, 1); + } + + public void testClear() throws Exception { + mStreamA.join(mGroup); + mStreamB.join(mGroup); + mGroup.clear(); + + assertEquals(mGroup.getStreams().length, 0); + assertFalse(mStreamA.isBusy()); + assertFalse(mStreamB.isBusy()); + } + + public void testDoubleClear() throws Exception { + mStreamA.join(mGroup); + mStreamB.join(mGroup); + mGroup.clear(); + mGroup.clear(); + } +} diff --git a/tests/cts/net/src/android/net/rtp/cts/AudioStreamTest.java b/tests/cts/net/src/android/net/rtp/cts/AudioStreamTest.java new file mode 100644 index 0000000000..f2db6ee9c4 --- /dev/null +++ b/tests/cts/net/src/android/net/rtp/cts/AudioStreamTest.java @@ -0,0 +1,96 @@ +/* + * 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 android.net.rtp.cts; + +import android.net.rtp.AudioCodec; +import android.net.rtp.AudioStream; +import android.platform.test.annotations.AppModeFull; +import android.test.AndroidTestCase; + +import java.net.InetAddress; + +@AppModeFull(reason = "RtpStream cannot create in instant app mode") +public class AudioStreamTest extends AndroidTestCase { + + private void testRtpStream(InetAddress address) throws Exception { + AudioStream stream = new AudioStream(address); + assertEquals(stream.getLocalAddress(), address); + assertEquals(stream.getLocalPort() % 2, 0); + + assertNull(stream.getRemoteAddress()); + assertEquals(stream.getRemotePort(), -1); + stream.associate(address, 1000); + assertEquals(stream.getRemoteAddress(), address); + assertEquals(stream.getRemotePort(), 1000); + + assertFalse(stream.isBusy()); + stream.release(); + } + + public void testV4Stream() throws Exception { + testRtpStream(InetAddress.getByName("127.0.0.1")); + } + + public void testV6Stream() throws Exception { + testRtpStream(InetAddress.getByName("::1")); + } + + public void testSetDtmfType() throws Exception { + AudioStream stream = new AudioStream(InetAddress.getByName("::1")); + + assertEquals(stream.getDtmfType(), -1); + try { + stream.setDtmfType(0); + fail("Expecting IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // ignore + } + stream.setDtmfType(96); + assertEquals(stream.getDtmfType(), 96); + + stream.setCodec(AudioCodec.getCodec(97, "PCMU/8000", null)); + try { + stream.setDtmfType(97); + fail("Expecting IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // ignore + } + stream.release(); + } + + public void testSetCodec() throws Exception { + AudioStream stream = new AudioStream(InetAddress.getByName("::1")); + + assertNull(stream.getCodec()); + stream.setCodec(AudioCodec.getCodec(97, "PCMU/8000", null)); + assertNotNull(stream.getCodec()); + + stream.setDtmfType(96); + try { + stream.setCodec(AudioCodec.getCodec(96, "PCMU/8000", null)); + fail("Expecting IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // ignore + } + stream.release(); + } + + public void testDoubleRelease() throws Exception { + AudioStream stream = new AudioStream(InetAddress.getByName("::1")); + stream.release(); + stream.release(); + } +} diff --git a/tests/cts/net/util/Android.bp b/tests/cts/net/util/Android.bp new file mode 100644 index 0000000000..c36d976423 --- /dev/null +++ b/tests/cts/net/util/Android.bp @@ -0,0 +1,26 @@ +// +// 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. +// + +// Common utilities for cts net tests. +java_library { + name: "cts-net-utils", + srcs: ["java/**/*.java", "java/**/*.kt"], + static_libs: [ + "compatibility-device-util-axt", + "junit", + "net-tests-utils", + ], +} \ No newline at end of file diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java new file mode 100644 index 0000000000..be0daae8dc --- /dev/null +++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java @@ -0,0 +1,693 @@ +/* + * 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.cts.util; + +import static android.Manifest.permission.NETWORK_SETTINGS; +import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_TEST; +import static android.net.wifi.WifiManager.SCAN_RESULTS_AVAILABLE_ACTION; + +import static com.android.testutils.TestPermissionUtil.runAsShell; + +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.annotation.NonNull; +import android.app.AppOpsManager; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.NetworkInfo.State; +import android.net.NetworkRequest; +import android.net.TestNetworkManager; +import android.net.wifi.ScanResult; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import android.os.SystemProperties; +import android.provider.Settings; +import android.system.Os; +import android.system.OsConstants; +import android.util.Log; + +import com.android.compatibility.common.util.SystemUtil; + +import junit.framework.AssertionFailedError; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public final class CtsNetUtils { + private static final String TAG = CtsNetUtils.class.getSimpleName(); + private static final int DURATION = 10000; + private static final int SOCKET_TIMEOUT_MS = 2000; + private static final int PRIVATE_DNS_PROBE_MS = 1_000; + + private static final int PRIVATE_DNS_SETTING_TIMEOUT_MS = 6_000; + private static final int CONNECTIVITY_CHANGE_TIMEOUT_SECS = 30; + public static final int HTTP_PORT = 80; + public static final String TEST_HOST = "connectivitycheck.gstatic.com"; + public static final String HTTP_REQUEST = + "GET /generate_204 HTTP/1.0\r\n" + + "Host: " + TEST_HOST + "\r\n" + + "Connection: keep-alive\r\n\r\n"; + // Action sent to ConnectivityActionReceiver when a network callback is sent via PendingIntent. + public static final String NETWORK_CALLBACK_ACTION = + "ConnectivityManagerTest.NetworkCallbackAction"; + + private final IBinder mBinder = new Binder(); + private final Context mContext; + private final ConnectivityManager mCm; + private final ContentResolver mCR; + private final WifiManager mWifiManager; + private TestNetworkCallback mCellNetworkCallback; + private String mOldPrivateDnsMode; + private String mOldPrivateDnsSpecifier; + + public CtsNetUtils(Context context) { + mContext = context; + mCm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + mCR = context.getContentResolver(); + } + + /** Checks if FEATURE_IPSEC_TUNNELS is enabled on the device */ + public boolean hasIpsecTunnelsFeature() { + return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS) + || SystemProperties.getInt("ro.product.first_api_level", 0) + >= Build.VERSION_CODES.Q; + } + + /** + * Sets the given appop using shell commands + * + *

Expects caller to hold the shell permission identity. + */ + public void setAppopPrivileged(int appop, boolean allow) { + final String opName = AppOpsManager.opToName(appop); + for (final String pkg : new String[] {"com.android.shell", mContext.getPackageName()}) { + final String cmd = + String.format( + "appops set %s %s %s", + pkg, // Package name + opName, // Appop + (allow ? "allow" : "deny")); // Action + SystemUtil.runShellCommand(cmd); + } + } + + /** Sets up a test network using the provided interface name */ + public TestNetworkCallback setupAndGetTestNetwork(String ifname) throws Exception { + // Build a network request + final NetworkRequest nr = + new NetworkRequest.Builder() + .clearCapabilities() + .addTransportType(TRANSPORT_TEST) + .setNetworkSpecifier(ifname) + .build(); + + final TestNetworkCallback cb = new TestNetworkCallback(); + mCm.requestNetwork(nr, cb); + + // Setup the test network after network request is filed to prevent Network from being + // reaped due to no requests matching it. + mContext.getSystemService(TestNetworkManager.class).setupTestNetwork(ifname, mBinder); + + return cb; + } + + // Toggle WiFi twice, leaving it in the state it started in + public void toggleWifi() { + if (mWifiManager.isWifiEnabled()) { + Network wifiNetwork = getWifiNetwork(); + disconnectFromWifi(wifiNetwork); + connectToWifi(); + } else { + connectToWifi(); + Network wifiNetwork = getWifiNetwork(); + disconnectFromWifi(wifiNetwork); + } + } + + /** + * Enable WiFi and wait for it to become connected to a network. + * + * This method expects to receive a legacy broadcast on connect, which may not be sent if the + * network does not become default or if it is not the first network. + */ + public Network connectToWifi() { + return connectToWifi(true /* expectLegacyBroadcast */); + } + + /** + * Enable WiFi and wait for it to become connected to a network. + * + * A network is considered connected when a {@link NetworkRequest} with TRANSPORT_WIFI + * receives a {@link NetworkCallback#onAvailable(Network)} callback. + */ + public Network ensureWifiConnected() { + return connectToWifi(false /* expectLegacyBroadcast */); + } + + /** + * Enable WiFi and wait for it to become connected to a network. + * + * @param expectLegacyBroadcast Whether to check for a legacy CONNECTIVITY_ACTION connected + * broadcast. The broadcast is typically not sent if the network + * does not become the default network, and is not the first + * network to appear. + * @return The network that was newly connected. + */ + private Network connectToWifi(boolean expectLegacyBroadcast) { + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(makeWifiNetworkRequest(), callback); + Network wifiNetwork = null; + + ConnectivityActionReceiver receiver = new ConnectivityActionReceiver( + mCm, ConnectivityManager.TYPE_WIFI, NetworkInfo.State.CONNECTED); + IntentFilter filter = new IntentFilter(); + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + mContext.registerReceiver(receiver, filter); + + boolean connected = false; + final String err = "Wifi must be configured to connect to an access point for this test."; + try { + clearWifiBlacklist(); + SystemUtil.runShellCommand("svc wifi enable"); + final WifiConfiguration config = maybeAddVirtualWifiConfiguration(); + if (config == null) { + // TODO: this may not clear the BSSID blacklist, as opposed to + // mWifiManager.connect(config) + assertTrue("Error reconnecting wifi", runAsShell(NETWORK_SETTINGS, + mWifiManager::reconnect)); + } else { + // When running CTS, devices are expected to have wifi networks pre-configured. + // This condition is only hit on virtual devices. + final Integer error = runAsShell(NETWORK_SETTINGS, () -> { + final ConnectWifiListener listener = new ConnectWifiListener(); + mWifiManager.connect(config, listener); + return listener.connectFuture.get( + CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS); + }); + assertNull("Error connecting to wifi: " + error, error); + } + // Ensure we get an onAvailable callback and possibly a CONNECTIVITY_ACTION. + wifiNetwork = callback.waitForAvailable(); + assertNotNull(err, wifiNetwork); + connected = !expectLegacyBroadcast || receiver.waitForState(); + } catch (InterruptedException ex) { + fail("connectToWifi was interrupted"); + } finally { + mCm.unregisterNetworkCallback(callback); + mContext.unregisterReceiver(receiver); + } + + assertTrue(err, connected); + return wifiNetwork; + } + + private static class ConnectWifiListener implements WifiManager.ActionListener { + /** + * Future completed when the connect process ends. Provides the error code or null if none. + */ + final CompletableFuture connectFuture = new CompletableFuture<>(); + @Override + public void onSuccess() { + connectFuture.complete(null); + } + + @Override + public void onFailure(int reason) { + connectFuture.complete(reason); + } + } + + private WifiConfiguration maybeAddVirtualWifiConfiguration() { + final List configs = runAsShell(NETWORK_SETTINGS, + mWifiManager::getConfiguredNetworks); + // If no network is configured, add a config for virtual access points if applicable + if (configs.size() == 0) { + final List scanResults = getWifiScanResults(); + final WifiConfiguration virtualConfig = maybeConfigureVirtualNetwork(scanResults); + assertNotNull("The device has no configured wifi network", virtualConfig); + + return virtualConfig; + } + // No need to add a configuration: there is already one + return null; + } + + private List getWifiScanResults() { + final CompletableFuture> scanResultsFuture = new CompletableFuture<>(); + runAsShell(NETWORK_SETTINGS, () -> { + final BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + scanResultsFuture.complete(mWifiManager.getScanResults()); + } + }; + mContext.registerReceiver(receiver, new IntentFilter(SCAN_RESULTS_AVAILABLE_ACTION)); + mWifiManager.startScan(); + }); + + try { + return scanResultsFuture.get(CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS); + } catch (ExecutionException | InterruptedException | TimeoutException e) { + throw new AssertionFailedError("Wifi scan results not received within timeout"); + } + } + + /** + * If a virtual wifi network is detected, add a configuration for that network. + * TODO(b/158150376): have the test infrastructure add virtual wifi networks when appropriate. + */ + private WifiConfiguration maybeConfigureVirtualNetwork(List scanResults) { + // Virtual wifi networks used on the emulator and cloud testing infrastructure + final List virtualSsids = Arrays.asList("VirtWifi", "AndroidWifi"); + Log.d(TAG, "Wifi scan results: " + scanResults); + final ScanResult virtualScanResult = scanResults.stream().filter( + s -> virtualSsids.contains(s.SSID)).findFirst().orElse(null); + + // Only add the virtual configuration if the virtual AP is detected in scans + if (virtualScanResult == null) return null; + + final WifiConfiguration virtualConfig = new WifiConfiguration(); + // ASCII SSIDs need to be surrounded by double quotes + virtualConfig.SSID = "\"" + virtualScanResult.SSID + "\""; + virtualConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE); + + runAsShell(NETWORK_SETTINGS, () -> { + final int networkId = mWifiManager.addNetwork(virtualConfig); + assertTrue(networkId >= 0); + assertTrue(mWifiManager.enableNetwork(networkId, false /* attemptConnect */)); + }); + return virtualConfig; + } + + /** + * Re-enable wifi networks that were blacklisted, typically because no internet connection was + * detected the last time they were connected. This is necessary to make sure wifi can reconnect + * to them. + */ + private void clearWifiBlacklist() { + runAsShell(NETWORK_SETTINGS, () -> { + for (WifiConfiguration cfg : mWifiManager.getConfiguredNetworks()) { + assertTrue(mWifiManager.enableNetwork(cfg.networkId, false /* attemptConnect */)); + } + }); + } + + /** + * Disable WiFi and wait for it to become disconnected from the network. + * + * This method expects to receive a legacy broadcast on disconnect, which may not be sent if the + * network was not default, or was not the first network. + * + * @param wifiNetworkToCheck If non-null, a network that should be disconnected. This network + * is expected to be able to establish a TCP connection to a remote + * server before disconnecting, and to have that connection closed in + * the process. + */ + public void disconnectFromWifi(Network wifiNetworkToCheck) { + disconnectFromWifi(wifiNetworkToCheck, true /* expectLegacyBroadcast */); + } + + /** + * Disable WiFi and wait for it to become disconnected from the network. + * + * @param wifiNetworkToCheck If non-null, a network that should be disconnected. This network + * is expected to be able to establish a TCP connection to a remote + * server before disconnecting, and to have that connection closed in + * the process. + */ + public void ensureWifiDisconnected(Network wifiNetworkToCheck) { + disconnectFromWifi(wifiNetworkToCheck, false /* expectLegacyBroadcast */); + } + + /** + * Disable WiFi and wait for it to become disconnected from the network. + * + * @param wifiNetworkToCheck If non-null, a network that should be disconnected. This network + * is expected to be able to establish a TCP connection to a remote + * server before disconnecting, and to have that connection closed in + * the process. + * @param expectLegacyBroadcast Whether to check for a legacy CONNECTIVITY_ACTION disconnected + * broadcast. The broadcast is typically not sent if the network + * was not the default network and not the first network to appear. + * The check will always be skipped if the device was not connected + * to wifi in the first place. + */ + private void disconnectFromWifi(Network wifiNetworkToCheck, boolean expectLegacyBroadcast) { + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(makeWifiNetworkRequest(), callback); + + ConnectivityActionReceiver receiver = new ConnectivityActionReceiver( + mCm, ConnectivityManager.TYPE_WIFI, NetworkInfo.State.DISCONNECTED); + IntentFilter filter = new IntentFilter(); + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + mContext.registerReceiver(receiver, filter); + + final WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); + final boolean wasWifiConnected = wifiInfo != null && wifiInfo.getNetworkId() != -1; + // Assert that we can establish a TCP connection on wifi. + Socket wifiBoundSocket = null; + if (wifiNetworkToCheck != null) { + assertTrue("Cannot check network " + wifiNetworkToCheck + ": wifi is not connected", + wasWifiConnected); + final NetworkCapabilities nc = mCm.getNetworkCapabilities(wifiNetworkToCheck); + assertNotNull("Network " + wifiNetworkToCheck + " is not connected", nc); + try { + wifiBoundSocket = getBoundSocket(wifiNetworkToCheck, TEST_HOST, HTTP_PORT); + testHttpRequest(wifiBoundSocket); + } catch (IOException e) { + fail("HTTP request before wifi disconnected failed with: " + e); + } + } + + try { + SystemUtil.runShellCommand("svc wifi disable"); + if (wasWifiConnected) { + // Ensure we get both an onLost callback and a CONNECTIVITY_ACTION. + assertNotNull("Did not receive onLost callback after disabling wifi", + callback.waitForLost()); + } + if (wasWifiConnected && expectLegacyBroadcast) { + assertTrue("Wifi failed to reach DISCONNECTED state.", receiver.waitForState()); + } + } catch (InterruptedException ex) { + fail("disconnectFromWifi was interrupted"); + } finally { + mCm.unregisterNetworkCallback(callback); + mContext.unregisterReceiver(receiver); + } + + // Check that the socket is closed when wifi disconnects. + if (wifiBoundSocket != null) { + try { + testHttpRequest(wifiBoundSocket); + fail("HTTP request should not succeed after wifi disconnects"); + } catch (IOException expected) { + assertEquals(Os.strerror(OsConstants.ECONNABORTED), expected.getMessage()); + } + } + } + + public Network getWifiNetwork() { + TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(makeWifiNetworkRequest(), callback); + Network network = null; + try { + network = callback.waitForAvailable(); + } catch (InterruptedException e) { + fail("NetworkCallback wait was interrupted."); + } finally { + mCm.unregisterNetworkCallback(callback); + } + assertNotNull("Cannot find Network for wifi. Is wifi connected?", network); + return network; + } + + public Network connectToCell() throws InterruptedException { + if (cellConnectAttempted()) { + throw new IllegalStateException("Already connected"); + } + NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_INTERNET) + .build(); + mCellNetworkCallback = new TestNetworkCallback(); + mCm.requestNetwork(cellRequest, mCellNetworkCallback); + final Network cellNetwork = mCellNetworkCallback.waitForAvailable(); + assertNotNull("Cell network not available. " + + "Please ensure the device has working mobile data.", cellNetwork); + return cellNetwork; + } + + public void disconnectFromCell() { + if (!cellConnectAttempted()) { + throw new IllegalStateException("Cell connection not attempted"); + } + mCm.unregisterNetworkCallback(mCellNetworkCallback); + mCellNetworkCallback = null; + } + + public boolean cellConnectAttempted() { + return mCellNetworkCallback != null; + } + + private NetworkRequest makeWifiNetworkRequest() { + return new NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .build(); + } + + private void testHttpRequest(Socket s) throws IOException { + OutputStream out = s.getOutputStream(); + InputStream in = s.getInputStream(); + + final byte[] requestBytes = HTTP_REQUEST.getBytes("UTF-8"); + byte[] responseBytes = new byte[4096]; + out.write(requestBytes); + in.read(responseBytes); + assertTrue(new String(responseBytes, "UTF-8").startsWith("HTTP/1.0 204 No Content\r\n")); + } + + private Socket getBoundSocket(Network network, String host, int port) throws IOException { + InetSocketAddress addr = new InetSocketAddress(host, port); + Socket s = network.getSocketFactory().createSocket(); + try { + s.setSoTimeout(SOCKET_TIMEOUT_MS); + s.connect(addr, SOCKET_TIMEOUT_MS); + } catch (IOException e) { + s.close(); + throw e; + } + return s; + } + + public void storePrivateDnsSetting() { + // Store private DNS setting + mOldPrivateDnsMode = Settings.Global.getString(mCR, Settings.Global.PRIVATE_DNS_MODE); + mOldPrivateDnsSpecifier = Settings.Global.getString(mCR, + Settings.Global.PRIVATE_DNS_SPECIFIER); + // It's possible that there is no private DNS default value in Settings. + // Give it a proper default mode which is opportunistic mode. + if (mOldPrivateDnsMode == null) { + mOldPrivateDnsSpecifier = ""; + mOldPrivateDnsMode = PRIVATE_DNS_MODE_OPPORTUNISTIC; + Settings.Global.putString(mCR, + Settings.Global.PRIVATE_DNS_SPECIFIER, mOldPrivateDnsSpecifier); + Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, mOldPrivateDnsMode); + } + } + + public void restorePrivateDnsSetting() throws InterruptedException { + if (mOldPrivateDnsMode == null || mOldPrivateDnsSpecifier == null) { + return; + } + // restore private DNS setting + if ("hostname".equals(mOldPrivateDnsMode)) { + setPrivateDnsStrictMode(mOldPrivateDnsSpecifier); + awaitPrivateDnsSetting("restorePrivateDnsSetting timeout", + mCm.getActiveNetwork(), + mOldPrivateDnsSpecifier, true); + } else { + Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, mOldPrivateDnsMode); + } + } + + public void setPrivateDnsStrictMode(String server) { + // To reduce flake rate, set PRIVATE_DNS_SPECIFIER before PRIVATE_DNS_MODE. This ensures + // that if the previous private DNS mode was not "hostname", the system only sees one + // EVENT_PRIVATE_DNS_SETTINGS_CHANGED event instead of two. + Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_SPECIFIER, server); + final String mode = Settings.Global.getString(mCR, Settings.Global.PRIVATE_DNS_MODE); + // If current private DNS mode is "hostname", we only need to set PRIVATE_DNS_SPECIFIER. + if (!"hostname".equals(mode)) { + Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, "hostname"); + } + } + + public void awaitPrivateDnsSetting(@NonNull String msg, @NonNull Network network, + @NonNull String server, boolean requiresValidatedServers) throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build(); + NetworkCallback callback = new NetworkCallback() { + @Override + public void onLinkPropertiesChanged(Network n, LinkProperties lp) { + if (requiresValidatedServers && lp.getValidatedPrivateDnsServers().isEmpty()) { + return; + } + if (network.equals(n) && server.equals(lp.getPrivateDnsServerName())) { + latch.countDown(); + } + } + }; + mCm.registerNetworkCallback(request, callback); + assertTrue(msg, latch.await(PRIVATE_DNS_SETTING_TIMEOUT_MS, TimeUnit.MILLISECONDS)); + mCm.unregisterNetworkCallback(callback); + // Wait some time for NetworkMonitor's private DNS probe to complete. If we do not do + // this, then the test could complete before the NetworkMonitor private DNS probe + // completes. This would result in tearDown disabling private DNS, and the NetworkMonitor + // private DNS probe getting stuck because there are no longer any private DNS servers to + // query. This then results in the next test not being able to change the private DNS + // setting within the timeout, because the NetworkMonitor thread is blocked in the + // private DNS probe. There is no way to know when the probe has completed: because the + // network is likely already validated, there is no callback that we can listen to, so + // just sleep. + if (requiresValidatedServers) { + Thread.sleep(PRIVATE_DNS_PROBE_MS); + } + } + + /** + * Receiver that captures the last connectivity change's network type and state. Recognizes + * both {@code CONNECTIVITY_ACTION} and {@code NETWORK_CALLBACK_ACTION} intents. + */ + public static class ConnectivityActionReceiver extends BroadcastReceiver { + + private final CountDownLatch mReceiveLatch = new CountDownLatch(1); + + private final int mNetworkType; + private final NetworkInfo.State mNetState; + private final ConnectivityManager mCm; + + public ConnectivityActionReceiver(ConnectivityManager cm, int networkType, + NetworkInfo.State netState) { + this.mCm = cm; + mNetworkType = networkType; + mNetState = netState; + } + + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + NetworkInfo networkInfo = null; + + // When receiving ConnectivityManager.CONNECTIVITY_ACTION, the NetworkInfo parcelable + // is stored in EXTRA_NETWORK_INFO. With a NETWORK_CALLBACK_ACTION, the Network is + // sent in EXTRA_NETWORK and we need to ask the ConnectivityManager for the NetworkInfo. + if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) { + networkInfo = intent.getExtras() + .getParcelable(ConnectivityManager.EXTRA_NETWORK_INFO); + assertNotNull("ConnectivityActionReceiver expected EXTRA_NETWORK_INFO", + networkInfo); + } else if (NETWORK_CALLBACK_ACTION.equals(action)) { + Network network = intent.getExtras() + .getParcelable(ConnectivityManager.EXTRA_NETWORK); + assertNotNull("ConnectivityActionReceiver expected EXTRA_NETWORK", network); + networkInfo = this.mCm.getNetworkInfo(network); + if (networkInfo == null) { + // When disconnecting, it seems like we get an intent sent with an invalid + // Network; that is, by the time we call ConnectivityManager.getNetworkInfo(), + // it is invalid. Ignore these. + Log.i(TAG, "ConnectivityActionReceiver NETWORK_CALLBACK_ACTION ignoring " + + "invalid network"); + return; + } + } else { + fail("ConnectivityActionReceiver received unxpected intent action: " + action); + } + + assertNotNull("ConnectivityActionReceiver didn't find NetworkInfo", networkInfo); + int networkType = networkInfo.getType(); + State networkState = networkInfo.getState(); + Log.i(TAG, "Network type: " + networkType + " state: " + networkState); + if (networkType == mNetworkType && networkInfo.getState() == mNetState) { + mReceiveLatch.countDown(); + } + } + + public boolean waitForState() throws InterruptedException { + return mReceiveLatch.await(CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS); + } + } + + /** + * Callback used in testRegisterNetworkCallback that allows caller to block on + * {@code onAvailable}. + */ + public static class TestNetworkCallback extends ConnectivityManager.NetworkCallback { + private final CountDownLatch mAvailableLatch = new CountDownLatch(1); + private final CountDownLatch mLostLatch = new CountDownLatch(1); + private final CountDownLatch mUnavailableLatch = new CountDownLatch(1); + + public Network currentNetwork; + public Network lastLostNetwork; + + public Network waitForAvailable() throws InterruptedException { + return mAvailableLatch.await(CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS) + ? currentNetwork : null; + } + + public Network waitForLost() throws InterruptedException { + return mLostLatch.await(CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS) + ? lastLostNetwork : null; + } + + public boolean waitForUnavailable() throws InterruptedException { + return mUnavailableLatch.await(2, TimeUnit.SECONDS); + } + + + @Override + public void onAvailable(Network network) { + currentNetwork = network; + mAvailableLatch.countDown(); + } + + @Override + public void onLost(Network network) { + lastLostNetwork = network; + if (network.equals(currentNetwork)) { + currentNetwork = null; + } + mLostLatch.countDown(); + } + + @Override + public void onUnavailable() { + mUnavailableLatch.countDown(); + } + } +} diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java new file mode 100644 index 0000000000..c95dc28dd1 --- /dev/null +++ b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java @@ -0,0 +1,447 @@ +/* + * 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.cts.util; + +import static android.net.TetheringManager.TETHERING_WIFI; +import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR; +import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_FAILED; +import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED; +import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED; + +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.junit.Assume.assumeTrue; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.net.Network; +import android.net.TetheredClient; +import android.net.TetheringManager; +import android.net.TetheringManager.TetheringEventCallback; +import android.net.TetheringManager.TetheringInterfaceRegexps; +import android.net.TetheringManager.TetheringRequest; +import android.net.wifi.WifiClient; +import android.net.wifi.WifiManager; +import android.net.wifi.WifiManager.SoftApCallback; +import android.os.ConditionVariable; + +import androidx.annotation.NonNull; + +import com.android.compatibility.common.util.SystemUtil; +import com.android.net.module.util.ArrayTrackRecord; + +import java.util.Collection; +import java.util.List; + +public final class CtsTetheringUtils { + private TetheringManager mTm; + private WifiManager mWm; + private Context mContext; + + private static final int DEFAULT_TIMEOUT_MS = 60_000; + + public CtsTetheringUtils(Context ctx) { + mContext = ctx; + mTm = mContext.getSystemService(TetheringManager.class); + mWm = mContext.getSystemService(WifiManager.class); + } + + public static class StartTetheringCallback implements TetheringManager.StartTetheringCallback { + private static int TIMEOUT_MS = 30_000; + public static class CallbackValue { + public final int error; + + private CallbackValue(final int e) { + error = e; + } + + public static class OnTetheringStarted extends CallbackValue { + OnTetheringStarted() { super(TETHER_ERROR_NO_ERROR); } + } + + public static class OnTetheringFailed extends CallbackValue { + OnTetheringFailed(final int error) { super(error); } + } + + @Override + public String toString() { + return String.format("%s(%d)", getClass().getSimpleName(), error); + } + } + + private final ArrayTrackRecord.ReadHead mHistory = + new ArrayTrackRecord().newReadHead(); + + @Override + public void onTetheringStarted() { + mHistory.add(new CallbackValue.OnTetheringStarted()); + } + + @Override + public void onTetheringFailed(final int error) { + mHistory.add(new CallbackValue.OnTetheringFailed(error)); + } + + public void verifyTetheringStarted() { + final CallbackValue cv = mHistory.poll(TIMEOUT_MS, c -> true); + assertNotNull("No onTetheringStarted after " + TIMEOUT_MS + " ms", cv); + assertTrue("Fail start tethering:" + cv, + cv instanceof CallbackValue.OnTetheringStarted); + } + + public void expectTetheringFailed(final int expected) throws InterruptedException { + final CallbackValue cv = mHistory.poll(TIMEOUT_MS, c -> true); + assertNotNull("No onTetheringFailed after " + TIMEOUT_MS + " ms", cv); + assertTrue("Expect fail with error code " + expected + ", but received: " + cv, + (cv instanceof CallbackValue.OnTetheringFailed) && (cv.error == expected)); + } + } + + public static boolean isIfaceMatch(final List ifaceRegexs, final List ifaces) { + return isIfaceMatch(ifaceRegexs.toArray(new String[0]), ifaces); + } + + public static boolean isIfaceMatch(final String[] ifaceRegexs, final List ifaces) { + if (ifaceRegexs == null) fail("ifaceRegexs should not be null"); + + if (ifaces == null) return false; + + for (String s : ifaces) { + for (String regex : ifaceRegexs) { + if (s.matches(regex)) { + return true; + } + } + } + return false; + } + + // Must poll the callback before looking at the member. + public static class TestTetheringEventCallback implements TetheringEventCallback { + private static final int TIMEOUT_MS = 30_000; + + public enum CallbackType { + ON_SUPPORTED, + ON_UPSTREAM, + ON_TETHERABLE_REGEX, + ON_TETHERABLE_IFACES, + ON_TETHERED_IFACES, + ON_ERROR, + ON_CLIENTS, + ON_OFFLOAD_STATUS, + }; + + public static class CallbackValue { + public final CallbackType callbackType; + public final Object callbackParam; + public final int callbackParam2; + + private CallbackValue(final CallbackType type, final Object param, final int param2) { + this.callbackType = type; + this.callbackParam = param; + this.callbackParam2 = param2; + } + } + + private final ArrayTrackRecord mHistory = + new ArrayTrackRecord(); + + private final ArrayTrackRecord.ReadHead mCurrent = + mHistory.newReadHead(); + + private TetheringInterfaceRegexps mTetherableRegex; + private List mTetherableIfaces; + private List mTetheredIfaces; + + @Override + public void onTetheringSupported(boolean supported) { + mHistory.add(new CallbackValue(CallbackType.ON_SUPPORTED, null, (supported ? 1 : 0))); + } + + @Override + public void onUpstreamChanged(Network network) { + mHistory.add(new CallbackValue(CallbackType.ON_UPSTREAM, network, 0)); + } + + @Override + public void onTetherableInterfaceRegexpsChanged(TetheringInterfaceRegexps reg) { + mTetherableRegex = reg; + mHistory.add(new CallbackValue(CallbackType.ON_TETHERABLE_REGEX, reg, 0)); + } + + @Override + public void onTetherableInterfacesChanged(List interfaces) { + mTetherableIfaces = interfaces; + mHistory.add(new CallbackValue(CallbackType.ON_TETHERABLE_IFACES, interfaces, 0)); + } + + @Override + public void onTetheredInterfacesChanged(List interfaces) { + mTetheredIfaces = interfaces; + mHistory.add(new CallbackValue(CallbackType.ON_TETHERED_IFACES, interfaces, 0)); + } + + @Override + public void onError(String ifName, int error) { + mHistory.add(new CallbackValue(CallbackType.ON_ERROR, ifName, error)); + } + + @Override + public void onClientsChanged(Collection clients) { + mHistory.add(new CallbackValue(CallbackType.ON_CLIENTS, clients, 0)); + } + + @Override + public void onOffloadStatusChanged(int status) { + mHistory.add(new CallbackValue(CallbackType.ON_OFFLOAD_STATUS, status, 0)); + } + + public void expectTetherableInterfacesChanged(@NonNull List regexs) { + assertNotNull("No expected tetherable ifaces callback", mCurrent.poll(TIMEOUT_MS, + (cv) -> { + if (cv.callbackType != CallbackType.ON_TETHERABLE_IFACES) return false; + final List interfaces = (List) cv.callbackParam; + return isIfaceMatch(regexs, interfaces); + })); + } + + public void expectTetheredInterfacesChanged(@NonNull List regexs) { + assertNotNull("No expected tethered ifaces callback", mCurrent.poll(TIMEOUT_MS, + (cv) -> { + if (cv.callbackType != CallbackType.ON_TETHERED_IFACES) return false; + + final List interfaces = (List) cv.callbackParam; + + // Null regexs means no active tethering. + if (regexs == null) return interfaces.isEmpty(); + + return isIfaceMatch(regexs, interfaces); + })); + } + + public void expectCallbackStarted() { + int receivedBitMap = 0; + // The each bit represent a type from CallbackType.ON_*. + // Expect all of callbacks except for ON_ERROR. + final int expectedBitMap = 0xff ^ (1 << CallbackType.ON_ERROR.ordinal()); + // Receive ON_ERROR on started callback is not matter. It just means tethering is + // failed last time, should able to continue the test this time. + while ((receivedBitMap & expectedBitMap) != expectedBitMap) { + final CallbackValue cv = mCurrent.poll(TIMEOUT_MS, c -> true); + if (cv == null) { + fail("No expected callbacks, " + "expected bitmap: " + + expectedBitMap + ", actual: " + receivedBitMap); + } + + receivedBitMap |= (1 << cv.callbackType.ordinal()); + } + } + + public void expectOneOfOffloadStatusChanged(int... offloadStatuses) { + assertNotNull("No offload status changed", mCurrent.poll(TIMEOUT_MS, (cv) -> { + if (cv.callbackType != CallbackType.ON_OFFLOAD_STATUS) return false; + + final int status = (int) cv.callbackParam; + for (int offloadStatus : offloadStatuses) { + if (offloadStatus == status) return true; + } + + return false; + })); + } + + public void expectErrorOrTethered(final String iface) { + assertNotNull("No expected callback", mCurrent.poll(TIMEOUT_MS, (cv) -> { + if (cv.callbackType == CallbackType.ON_ERROR + && iface.equals((String) cv.callbackParam)) { + return true; + } + if (cv.callbackType == CallbackType.ON_TETHERED_IFACES + && ((List) cv.callbackParam).contains(iface)) { + return true; + } + + return false; + })); + } + + public Network getCurrentValidUpstream() { + final CallbackValue result = mCurrent.poll(TIMEOUT_MS, (cv) -> { + return (cv.callbackType == CallbackType.ON_UPSTREAM) + && cv.callbackParam != null; + }); + + assertNotNull("No valid upstream", result); + return (Network) result.callbackParam; + } + + public void assumeTetheringSupported() { + final ArrayTrackRecord.ReadHead history = + mHistory.newReadHead(); + assertNotNull("No onSupported callback", history.poll(TIMEOUT_MS, (cv) -> { + if (cv.callbackType != CallbackType.ON_SUPPORTED) return false; + + assumeTrue(cv.callbackParam2 == 1 /* supported */); + return true; + })); + } + + public void assumeWifiTetheringSupported(final Context ctx) throws Exception { + assumeTetheringSupported(); + + assumeTrue(!getTetheringInterfaceRegexps().getTetherableWifiRegexs().isEmpty()); + + final PackageManager pm = ctx.getPackageManager(); + assumeTrue(pm.hasSystemFeature(PackageManager.FEATURE_WIFI)); + + WifiManager wm = ctx.getSystemService(WifiManager.class); + // Wifi feature flags only work when wifi is on. + final boolean previousWifiEnabledState = wm.isWifiEnabled(); + try { + if (!previousWifiEnabledState) SystemUtil.runShellCommand("svc wifi enable"); + waitForWifiEnabled(ctx); + assumeTrue(wm.isPortableHotspotSupported()); + } finally { + if (!previousWifiEnabledState) SystemUtil.runShellCommand("svc wifi disable"); + } + } + + public TetheringInterfaceRegexps getTetheringInterfaceRegexps() { + return mTetherableRegex; + } + + public List getTetherableInterfaces() { + return mTetherableIfaces; + } + + public List getTetheredInterfaces() { + return mTetheredIfaces; + } + } + + private static void waitForWifiEnabled(final Context ctx) throws Exception { + WifiManager wm = ctx.getSystemService(WifiManager.class); + if (wm.isWifiEnabled()) return; + + final ConditionVariable mWaiting = new ConditionVariable(); + final BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) { + if (wm.isWifiEnabled()) mWaiting.open(); + } + } + }; + try { + ctx.registerReceiver(receiver, new IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION)); + if (!mWaiting.block(DEFAULT_TIMEOUT_MS)) { + assertTrue("Wifi did not become enabled after " + DEFAULT_TIMEOUT_MS + "ms", + wm.isWifiEnabled()); + } + } finally { + ctx.unregisterReceiver(receiver); + } + } + + public TestTetheringEventCallback registerTetheringEventCallback() { + final TestTetheringEventCallback tetherEventCallback = + new TestTetheringEventCallback(); + + mTm.registerTetheringEventCallback(c -> c.run() /* executor */, tetherEventCallback); + tetherEventCallback.expectCallbackStarted(); + + return tetherEventCallback; + } + + public void unregisterTetheringEventCallback(final TestTetheringEventCallback callback) { + mTm.unregisterTetheringEventCallback(callback); + } + + private static List getWifiTetherableInterfaceRegexps( + final TestTetheringEventCallback callback) { + return callback.getTetheringInterfaceRegexps().getTetherableWifiRegexs(); + } + + public static boolean isWifiTetheringSupported(final TestTetheringEventCallback callback) { + return !getWifiTetherableInterfaceRegexps(callback).isEmpty(); + } + + public void startWifiTethering(final TestTetheringEventCallback callback) + throws InterruptedException { + final List wifiRegexs = getWifiTetherableInterfaceRegexps(callback); + assertFalse(isIfaceMatch(wifiRegexs, callback.getTetheredInterfaces())); + + final StartTetheringCallback startTetheringCallback = new StartTetheringCallback(); + final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI) + .setShouldShowEntitlementUi(false).build(); + mTm.startTethering(request, c -> c.run() /* executor */, startTetheringCallback); + startTetheringCallback.verifyTetheringStarted(); + + callback.expectTetheredInterfacesChanged(wifiRegexs); + + callback.expectOneOfOffloadStatusChanged( + TETHER_HARDWARE_OFFLOAD_STARTED, + TETHER_HARDWARE_OFFLOAD_FAILED); + } + + private static class StopSoftApCallback implements SoftApCallback { + private final ConditionVariable mWaiting = new ConditionVariable(); + @Override + public void onStateChanged(int state, int failureReason) { + if (state == WifiManager.WIFI_AP_STATE_DISABLED) mWaiting.open(); + } + + @Override + public void onConnectedClientsChanged(List clients) { } + + public void waitForSoftApStopped() { + if (!mWaiting.block(DEFAULT_TIMEOUT_MS)) { + fail("stopSoftAp Timeout"); + } + } + } + + // Wait for softAp to be disabled. This is necessary on devices where stopping softAp + // deletes the interface. On these devices, tethering immediately stops when the softAp + // interface is removed, but softAp is not yet fully disabled. Wait for softAp to be + // fully disabled, because otherwise the next test might fail because it attempts to + // start softAp before it's fully stopped. + public void expectSoftApDisabled() { + final StopSoftApCallback callback = new StopSoftApCallback(); + try { + mWm.registerSoftApCallback(c -> c.run(), callback); + // registerSoftApCallback will immediately call the callback with the current state, so + // this callback will fire even if softAp is already disabled. + callback.waitForSoftApStopped(); + } finally { + mWm.unregisterSoftApCallback(callback); + } + } + + public void stopWifiTethering(final TestTetheringEventCallback callback) { + mTm.stopTethering(TETHERING_WIFI); + expectSoftApDisabled(); + callback.expectTetheredInterfacesChanged(null); + callback.expectOneOfOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED); + } +} diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp new file mode 100644 index 0000000000..b1d4a6052b --- /dev/null +++ b/tests/cts/tethering/Android.bp @@ -0,0 +1,56 @@ +// 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. + +android_test { + name: "CtsTetheringTest", + defaults: ["cts_defaults"], + + libs: [ + "android.test.base", + ], + + srcs: [ + "src/**/*.java", + ], + + static_libs: [ + "TetheringCommonTests", + "TetheringIntegrationTestsLib", + "compatibility-device-util-axt", + "cts-net-utils", + "net-tests-utils", + "ctstestrunner-axt", + "junit", + "junit-params", + ], + + jni_libs: [ + // For mockito extended + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], + + // Change to system current when TetheringManager move to bootclass path. + platform_apis: true, + + // Tag this module as a cts test artifact + test_suites: [ + "cts", + "general-tests", + "mts", + ], + + // Include both the 32 and 64 bit versions + compile_multilib: "both", +} diff --git a/tests/cts/tethering/AndroidManifest.xml b/tests/cts/tethering/AndroidManifest.xml new file mode 100644 index 0000000000..911dbf2ffb --- /dev/null +++ b/tests/cts/tethering/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + diff --git a/tests/cts/tethering/AndroidTest.xml b/tests/cts/tethering/AndroidTest.xml new file mode 100644 index 0000000000..e752e3a82a --- /dev/null +++ b/tests/cts/tethering/AndroidTest.xml @@ -0,0 +1,35 @@ + + + + diff --git a/tests/cts/tethering/OWNERS b/tests/cts/tethering/OWNERS new file mode 100644 index 0000000000..cd6abeb6e8 --- /dev/null +++ b/tests/cts/tethering/OWNERS @@ -0,0 +1,4 @@ +# Bug component: 31808 +lorenzo@google.com +satk@google.com + diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java new file mode 100644 index 0000000000..71a81ff621 --- /dev/null +++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java @@ -0,0 +1,463 @@ +/* + * 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.tethering.test; + +import static android.content.pm.PackageManager.FEATURE_TELEPHONY; +import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; +import static android.net.TetheringManager.TETHERING_USB; +import static android.net.TetheringManager.TETHERING_WIFI; +import static android.net.TetheringManager.TETHERING_WIFI_P2P; +import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN; +import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION; +import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR; +import static android.net.cts.util.CtsTetheringUtils.isIfaceMatch; +import static android.net.cts.util.CtsTetheringUtils.isWifiTetheringSupported; + +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.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + +import android.app.UiAutomation; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.LinkAddress; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.TetheringManager; +import android.net.TetheringManager.OnTetheringEntitlementResultListener; +import android.net.TetheringManager.TetheringInterfaceRegexps; +import android.net.TetheringManager.TetheringRequest; +import android.net.cts.util.CtsNetUtils; +import android.net.cts.util.CtsNetUtils.TestNetworkCallback; +import android.net.cts.util.CtsTetheringUtils; +import android.net.cts.util.CtsTetheringUtils.StartTetheringCallback; +import android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.os.PersistableBundle; +import android.os.ResultReceiver; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +@RunWith(AndroidJUnit4.class) +public class TetheringManagerTest { + + private Context mContext; + + private ConnectivityManager mCm; + private TetheringManager mTM; + private WifiManager mWm; + private PackageManager mPm; + + private TetherChangeReceiver mTetherChangeReceiver; + private CtsNetUtils mCtsNetUtils; + private CtsTetheringUtils mCtsTetheringUtils; + + private static final int DEFAULT_TIMEOUT_MS = 60_000; + + private void adoptShellPermissionIdentity() { + final UiAutomation uiAutomation = + InstrumentationRegistry.getInstrumentation().getUiAutomation(); + uiAutomation.adoptShellPermissionIdentity(); + } + + private void dropShellPermissionIdentity() { + final UiAutomation uiAutomation = + InstrumentationRegistry.getInstrumentation().getUiAutomation(); + uiAutomation.dropShellPermissionIdentity(); + } + + @Before + public void setUp() throws Exception { + adoptShellPermissionIdentity(); + mContext = InstrumentationRegistry.getContext(); + mCm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + mTM = (TetheringManager) mContext.getSystemService(Context.TETHERING_SERVICE); + mWm = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + mPm = mContext.getPackageManager(); + mCtsNetUtils = new CtsNetUtils(mContext); + mCtsTetheringUtils = new CtsTetheringUtils(mContext); + mTetherChangeReceiver = new TetherChangeReceiver(); + final IntentFilter filter = new IntentFilter( + TetheringManager.ACTION_TETHER_STATE_CHANGED); + final Intent intent = mContext.registerReceiver(mTetherChangeReceiver, filter); + if (intent != null) mTetherChangeReceiver.onReceive(null, intent); + } + + @After + public void tearDown() throws Exception { + mTM.stopAllTethering(); + mContext.unregisterReceiver(mTetherChangeReceiver); + dropShellPermissionIdentity(); + } + + private class TetherChangeReceiver extends BroadcastReceiver { + private class TetherState { + final ArrayList mAvailable; + final ArrayList mActive; + final ArrayList mErrored; + + TetherState(Intent intent) { + mAvailable = intent.getStringArrayListExtra( + TetheringManager.EXTRA_AVAILABLE_TETHER); + mActive = intent.getStringArrayListExtra( + TetheringManager.EXTRA_ACTIVE_TETHER); + mErrored = intent.getStringArrayListExtra( + TetheringManager.EXTRA_ERRORED_TETHER); + } + } + + @Override + public void onReceive(Context content, Intent intent) { + String action = intent.getAction(); + if (action.equals(TetheringManager.ACTION_TETHER_STATE_CHANGED)) { + mResult.add(new TetherState(intent)); + } + } + + public final LinkedBlockingQueue mResult = new LinkedBlockingQueue<>(); + + // Expects that tethering reaches the desired state. + // - If active is true, expects that tethering is enabled on at least one interface + // matching ifaceRegexs. + // - If active is false, expects that tethering is disabled on all the interfaces matching + // ifaceRegexs. + // Fails if any interface matching ifaceRegexs becomes errored. + public void expectTethering(final boolean active, final String[] ifaceRegexs) { + while (true) { + final TetherState state = pollAndAssertNoError(DEFAULT_TIMEOUT_MS, ifaceRegexs); + assertNotNull("Did not receive expected state change, active: " + active, state); + + if (isIfaceActive(ifaceRegexs, state) == active) return; + } + } + + private TetherState pollAndAssertNoError(final int timeout, final String[] ifaceRegexs) { + final TetherState state = pollTetherState(timeout); + assertNoErroredIfaces(state, ifaceRegexs); + return state; + } + + private TetherState pollTetherState(final int timeout) { + try { + return mResult.poll(timeout, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + fail("No result after " + timeout + " ms"); + return null; + } + } + + private boolean isIfaceActive(final String[] ifaceRegexs, final TetherState state) { + return isIfaceMatch(ifaceRegexs, state.mActive); + } + + private void assertNoErroredIfaces(final TetherState state, final String[] ifaceRegexs) { + if (state == null || state.mErrored == null) return; + + if (isIfaceMatch(ifaceRegexs, state.mErrored)) { + fail("Found failed tethering interfaces: " + Arrays.toString(state.mErrored.toArray())); + } + } + } + + @Test + public void testStartTetheringWithStateChangeBroadcast() throws Exception { + final TestTetheringEventCallback tetherEventCallback = + mCtsTetheringUtils.registerTetheringEventCallback(); + try { + tetherEventCallback.assumeWifiTetheringSupported(mContext); + } finally { + mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback); + } + + final String[] wifiRegexs = mTM.getTetherableWifiRegexs(); + final StartTetheringCallback startTetheringCallback = new StartTetheringCallback(); + final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI) + .setShouldShowEntitlementUi(false).build(); + mTM.startTethering(request, c -> c.run() /* executor */, startTetheringCallback); + startTetheringCallback.verifyTetheringStarted(); + + mTetherChangeReceiver.expectTethering(true /* active */, wifiRegexs); + + mTM.stopTethering(TETHERING_WIFI); + mCtsTetheringUtils.expectSoftApDisabled(); + mTetherChangeReceiver.expectTethering(false /* active */, wifiRegexs); + } + + @Test + public void testTetheringRequest() { + final TetheringRequest tr = new TetheringRequest.Builder(TETHERING_WIFI).build(); + assertEquals(TETHERING_WIFI, tr.getTetheringType()); + assertNull(tr.getLocalIpv4Address()); + assertNull(tr.getClientStaticIpv4Address()); + assertFalse(tr.isExemptFromEntitlementCheck()); + assertTrue(tr.getShouldShowEntitlementUi()); + + final LinkAddress localAddr = new LinkAddress("192.168.24.5/24"); + final LinkAddress clientAddr = new LinkAddress("192.168.24.100/24"); + final TetheringRequest tr2 = new TetheringRequest.Builder(TETHERING_USB) + .setStaticIpv4Addresses(localAddr, clientAddr) + .setExemptFromEntitlementCheck(true) + .setShouldShowEntitlementUi(false).build(); + + assertEquals(localAddr, tr2.getLocalIpv4Address()); + assertEquals(clientAddr, tr2.getClientStaticIpv4Address()); + assertEquals(TETHERING_USB, tr2.getTetheringType()); + assertTrue(tr2.isExemptFromEntitlementCheck()); + assertFalse(tr2.getShouldShowEntitlementUi()); + } + + @Test + public void testRegisterTetheringEventCallback() throws Exception { + final TestTetheringEventCallback tetherEventCallback = + mCtsTetheringUtils.registerTetheringEventCallback(); + + try { + tetherEventCallback.assumeWifiTetheringSupported(mContext); + + mCtsTetheringUtils.startWifiTethering(tetherEventCallback); + + final List tetheredIfaces = tetherEventCallback.getTetheredInterfaces(); + assertEquals(1, tetheredIfaces.size()); + final String wifiTetheringIface = tetheredIfaces.get(0); + + mCtsTetheringUtils.stopWifiTethering(tetherEventCallback); + + try { + final int ret = mTM.tether(wifiTetheringIface); + // There is no guarantee that the wifi interface will be available after disabling + // the hotspot, so don't fail the test if the call to tether() fails. + if (ret == TETHER_ERROR_NO_ERROR) { + // If calling #tether successful, there is a callback to tell the result of + // tethering setup. + tetherEventCallback.expectErrorOrTethered(wifiTetheringIface); + } + } finally { + mTM.untether(wifiTetheringIface); + } + } finally { + mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback); + } + } + + @Test + public void testGetTetherableInterfaceRegexps() { + final TestTetheringEventCallback tetherEventCallback = + mCtsTetheringUtils.registerTetheringEventCallback(); + tetherEventCallback.assumeTetheringSupported(); + + final TetheringInterfaceRegexps tetherableRegexs = + tetherEventCallback.getTetheringInterfaceRegexps(); + final List wifiRegexs = tetherableRegexs.getTetherableWifiRegexs(); + final List usbRegexs = tetherableRegexs.getTetherableUsbRegexs(); + final List btRegexs = tetherableRegexs.getTetherableBluetoothRegexs(); + + assertEquals(wifiRegexs, Arrays.asList(mTM.getTetherableWifiRegexs())); + assertEquals(usbRegexs, Arrays.asList(mTM.getTetherableUsbRegexs())); + assertEquals(btRegexs, Arrays.asList(mTM.getTetherableBluetoothRegexs())); + + //Verify that any regex name should only contain in one array. + wifiRegexs.forEach(s -> assertFalse(usbRegexs.contains(s))); + wifiRegexs.forEach(s -> assertFalse(btRegexs.contains(s))); + usbRegexs.forEach(s -> assertFalse(btRegexs.contains(s))); + + mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback); + } + + @Test + public void testStopAllTethering() throws Exception { + final TestTetheringEventCallback tetherEventCallback = + mCtsTetheringUtils.registerTetheringEventCallback(); + try { + tetherEventCallback.assumeWifiTetheringSupported(mContext); + + // TODO: start ethernet tethering here when TetheringManagerTest is moved to + // TetheringIntegrationTest. + + mCtsTetheringUtils.startWifiTethering(tetherEventCallback); + + mTM.stopAllTethering(); + tetherEventCallback.expectTetheredInterfacesChanged(null); + } finally { + mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback); + } + } + + @Test + public void testEnableTetheringPermission() throws Exception { + dropShellPermissionIdentity(); + final StartTetheringCallback startTetheringCallback = new StartTetheringCallback(); + mTM.startTethering(new TetheringRequest.Builder(TETHERING_WIFI).build(), + c -> c.run() /* executor */, startTetheringCallback); + startTetheringCallback.expectTetheringFailed(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION); + } + + private class EntitlementResultListener implements OnTetheringEntitlementResultListener { + private final CompletableFuture future = new CompletableFuture<>(); + + @Override + public void onTetheringEntitlementResult(int result) { + future.complete(result); + } + + public int get(long timeout, TimeUnit unit) throws Exception { + return future.get(timeout, unit); + } + + } + + private void assertEntitlementResult(final Consumer functor, + final int expect) throws Exception { + final EntitlementResultListener listener = new EntitlementResultListener(); + functor.accept(listener); + + assertEquals(expect, listener.get(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS)); + } + + @Test + public void testRequestLatestEntitlementResult() throws Exception { + assumeTrue(mTM.isTetheringSupported()); + // Verify that requestLatestTetheringEntitlementResult() can get entitlement + // result(TETHER_ERROR_ENTITLEMENT_UNKNOWN due to invalid downstream type) via listener. + assertEntitlementResult(listener -> mTM.requestLatestTetheringEntitlementResult( + TETHERING_WIFI_P2P, false, c -> c.run(), listener), + TETHER_ERROR_ENTITLEMENT_UNKNOWN); + + // Verify that requestLatestTetheringEntitlementResult() can get entitlement + // result(TETHER_ERROR_ENTITLEMENT_UNKNOWN due to invalid downstream type) via receiver. + assertEntitlementResult(listener -> mTM.requestLatestTetheringEntitlementResult( + TETHERING_WIFI_P2P, + new ResultReceiver(null /* handler */) { + @Override + public void onReceiveResult(int resultCode, Bundle resultData) { + listener.onTetheringEntitlementResult(resultCode); + } + }, false), + TETHER_ERROR_ENTITLEMENT_UNKNOWN); + + // Do not request TETHERING_WIFI entitlement result if TETHERING_WIFI is not available. + assumeTrue(mTM.getTetherableWifiRegexs().length > 0); + + // Verify that null listener will cause IllegalArgumentException. + try { + mTM.requestLatestTetheringEntitlementResult( + TETHERING_WIFI, false, c -> c.run(), null); + } catch (IllegalArgumentException expect) { } + + // Override carrier config to ignore entitlement check. + final PersistableBundle bundle = new PersistableBundle(); + bundle.putBoolean(CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, false); + overrideCarrierConfig(bundle); + + // Verify that requestLatestTetheringEntitlementResult() can get entitlement + // result TETHER_ERROR_NO_ERROR due to provisioning bypassed. + assertEntitlementResult(listener -> mTM.requestLatestTetheringEntitlementResult( + TETHERING_WIFI, false, c -> c.run(), listener), TETHER_ERROR_NO_ERROR); + + // Reset carrier config. + overrideCarrierConfig(null); + } + + private void overrideCarrierConfig(PersistableBundle bundle) { + final CarrierConfigManager configManager = (CarrierConfigManager) mContext + .getSystemService(Context.CARRIER_CONFIG_SERVICE); + final int subId = SubscriptionManager.getDefaultSubscriptionId(); + configManager.overrideConfig(subId, bundle); + } + + @Test + public void testTetheringUpstream() throws Exception { + assumeTrue(mPm.hasSystemFeature(FEATURE_TELEPHONY)); + final TestTetheringEventCallback tetherEventCallback = + mCtsTetheringUtils.registerTetheringEventCallback(); + + boolean previousWifiEnabledState = false; + + try { + tetherEventCallback.assumeWifiTetheringSupported(mContext); + + previousWifiEnabledState = mWm.isWifiEnabled(); + if (previousWifiEnabledState) { + mCtsNetUtils.ensureWifiDisconnected(null); + } + + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + Network activeNetwork = null; + try { + mCm.registerDefaultNetworkCallback(networkCallback); + activeNetwork = networkCallback.waitForAvailable(); + } finally { + mCm.unregisterNetworkCallback(networkCallback); + } + + assertNotNull("No active network. Please ensure the device has working mobile data.", + activeNetwork); + final NetworkCapabilities activeNetCap = mCm.getNetworkCapabilities(activeNetwork); + + // If active nework is ETHERNET, tethering may not use cell network as upstream. + assumeFalse(activeNetCap.hasTransport(TRANSPORT_ETHERNET)); + + assertTrue(activeNetCap.hasTransport(TRANSPORT_CELLULAR)); + + mCtsTetheringUtils.startWifiTethering(tetherEventCallback); + + final TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService( + Context.TELEPHONY_SERVICE); + final boolean dunRequired = telephonyManager.isTetheringApnRequired(); + final int expectedCap = dunRequired ? NET_CAPABILITY_DUN : NET_CAPABILITY_INTERNET; + final Network network = tetherEventCallback.getCurrentValidUpstream(); + final NetworkCapabilities netCap = mCm.getNetworkCapabilities(network); + assertTrue(netCap.hasTransport(TRANSPORT_CELLULAR)); + assertTrue(netCap.hasCapability(expectedCap)); + + mCtsTetheringUtils.stopWifiTethering(tetherEventCallback); + } finally { + mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback); + if (previousWifiEnabledState) { + mCtsNetUtils.connectToWifi(); + } + } + } +}